From b6cb5831c2b5f30e596cd286db6ebadb807965f6 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Tue, 9 Dec 2025 13:49:20 +0530 Subject: [PATCH 01/24] Refactor Prisma client usage and enhance service integration for improved connection management --- layers/prisma/nodejs/package.json | 5 ++-- prisma/prisma.ts | 6 ++-- serverless.yml | 14 ++++++---- src/common/database/prisma.client.ts | 28 +++++++++++++++---- src/common/database/prisma.lambda.service.ts | 23 ++------------- src/common/database/prisma.service.ts | 7 +++++ .../services/amNotification.service.ts | 4 +-- 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/layers/prisma/nodejs/package.json b/layers/prisma/nodejs/package.json index 47d0046..5cd95ff 100644 --- a/layers/prisma/nodejs/package.json +++ b/layers/prisma/nodejs/package.json @@ -1,10 +1,11 @@ { "name": "prisma-layer", "version": "1.0.0", - "description": "Lambda layer for Prisma 7 with pg driver adapter", + "description": "Lambda layer for Prisma 7 with pg driver adapter and zod", "dependencies": { "@prisma/client": "^7.0.1", "@prisma/adapter-pg": "^7.0.1", - "pg": "^8.13.0" + "pg": "^8.13.0", + "zod": "^4.1.12" } } diff --git a/prisma/prisma.ts b/prisma/prisma.ts index 61913bb..966c5cd 100644 --- a/prisma/prisma.ts +++ b/prisma/prisma.ts @@ -1,8 +1,8 @@ // prisma.ts -import { PrismaClient } from '@prisma/client'; +// Re-export from the main singleton for consistency +import { prisma } from '../src/common/database/prisma.client'; -// The DATABASE_URL environment variable will be automatically used -export const prisma = new PrismaClient(); +export { prisma }; process.on('SIGINT', async () => { await prisma.$disconnect(); diff --git a/serverless.yml b/serverless.yml index f776226..d4210bc 100644 --- a/serverless.yml +++ b/serverless.yml @@ -82,11 +82,12 @@ build: platform: node # Mark as external so they're not bundled into the JS external: - # - '@prisma/client' - # - '.prisma/client' - # - '.prisma' + - '@prisma/client' + - '.prisma/client' + - '.prisma' - '@prisma/adapter-pg' - 'pg' + - 'zod' - '@aws-sdk/*' - '@smithy/*' - '@aws-crypto/*' @@ -97,10 +98,11 @@ build: - '@smithy/*' - '@aws-crypto/*' - '@prisma/adapter-pg' - # - '@prisma/client' - # - '.prisma' - # - '.prisma/client' + - '@prisma/client' + - '.prisma' + - '.prisma/client' - 'pg' + - 'zod' - 'pg-*' - 'postgres-*' - 'pgpass' diff --git a/src/common/database/prisma.client.ts b/src/common/database/prisma.client.ts index 1da7d2e..1831f66 100644 --- a/src/common/database/prisma.client.ts +++ b/src/common/database/prisma.client.ts @@ -1,11 +1,29 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +// Singleton pattern for Prisma client - prevents "Too many database connections" error +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; -export const prisma = new PrismaClient({ - adapter, - log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'], -}); +function createPrismaClient() { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + + return new PrismaClient({ + adapter, + log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'], + }); +} + +export const prisma = globalForPrisma.prisma ?? createPrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} + +// For serverless environments, always cache the client +if (process.env.IS_OFFLINE || process.env.AWS_LAMBDA_FUNCTION_NAME) { + globalForPrisma.prisma = prisma; +} diff --git a/src/common/database/prisma.lambda.service.ts b/src/common/database/prisma.lambda.service.ts index 0aa5b78..e5409b6 100644 --- a/src/common/database/prisma.lambda.service.ts +++ b/src/common/database/prisma.lambda.service.ts @@ -1,22 +1,5 @@ -import { PrismaClient } from '@prisma/client'; -import { PrismaPg } from '@prisma/adapter-pg'; - -const adapter = new PrismaPg({ - connectionString: process.env.DATABASE_URL!, -}); - -let prisma: PrismaClient; - -if (!(global as any).prisma) { - (global as any).prisma = new PrismaClient({ - adapter, - log: - process.env.NODE_ENV === 'dev' - ? ['query', 'info', 'warn', 'error'] - : ['error'], - }); -} - -prisma = (global as any).prisma; +// Re-export the singleton prisma client for Lambda handlers +// This ensures all Lambda functions use the same cached connection +import { prisma } from './prisma.client'; export const prismaClient = prisma; \ No newline at end of file diff --git a/src/common/database/prisma.service.ts b/src/common/database/prisma.service.ts index bb6565f..b75245f 100644 --- a/src/common/database/prisma.service.ts +++ b/src/common/database/prisma.service.ts @@ -1,8 +1,15 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; +import { prisma } from './prisma.client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + constructor() { + super(); + // Use the singleton instance + Object.assign(this, prisma); + } + async onModuleInit() { await this.$connect(); } diff --git a/src/modules/minglaradmin/services/amNotification.service.ts b/src/modules/minglaradmin/services/amNotification.service.ts index d38bf39..2990159 100644 --- a/src/modules/minglaradmin/services/amNotification.service.ts +++ b/src/modules/minglaradmin/services/amNotification.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../../common/database/prisma.lambda.service'; +import { prismaClient } from '../../../common/database/prisma.lambda.service'; import { sendAMEmailForHostAssign } from './AMEmail.service'; @Injectable() export class AMNotificationService { - constructor(private prisma: PrismaService) {} + private prisma = prismaClient; /** * Fetch account manager email by id and send assignment email. From 6166075967653652f2a217e51475afffb70f4ff6 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Tue, 9 Dec 2025 13:50:30 +0530 Subject: [PATCH 02/24] Update service name in serverless configuration to 'minglar' and add 'zod' dependency in package-lock.json --- layers/prisma/nodejs/package-lock.json | 12 +++++++++++- serverless.yml | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/layers/prisma/nodejs/package-lock.json b/layers/prisma/nodejs/package-lock.json index 389329f..861854b 100644 --- a/layers/prisma/nodejs/package-lock.json +++ b/layers/prisma/nodejs/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@prisma/adapter-pg": "^7.0.1", "@prisma/client": "^7.0.1", - "pg": "^8.13.0" + "pg": "^8.13.0", + "zod": "^4.1.12" } }, "node_modules/@prisma/adapter-pg": { @@ -223,6 +224,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/serverless.yml b/serverless.yml index d4210bc..9a388ab 100644 --- a/serverless.yml +++ b/serverless.yml @@ -1,4 +1,4 @@ -service: minglarDev +service: minglar useDotenv: true From 3652d851f7bb084d32091eda2d658e2ead059fae Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 9 Dec 2025 15:02:15 +0530 Subject: [PATCH 03/24] Enhance email notifications for host application approvals and rejections by including the host's first name in the greeting. --- .../handlers/hosthub/hosts/acceptHostApplication.ts | 2 +- .../handlers/hosthub/hosts/rejectHostApplicationAM.ts | 2 +- .../minglaradmin/services/approvalMailtoHost.service.ts | 3 ++- .../minglaradmin/services/rejectionMailtoHost.service.ts | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts index 803d32d..f3fef25 100644 --- a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts +++ b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts @@ -47,7 +47,7 @@ export const handler = safeHandler(async ( // Add suggestion using service await minglarService.acceptHostApplication(hostXid, userInfo.id); const hostDetails = await minglarService.getUserDetails(hostXid) - await sendEmailToHostForApprovedApplication(hostDetails.emailAddress) + await sendEmailToHostForApprovedApplication(hostDetails.emailAddress, hostDetails.firstName) return { statusCode: 200, diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts index 2539f75..4e47a59 100644 --- a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts +++ b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts @@ -47,7 +47,7 @@ export const handler = safeHandler(async ( // Add suggestion using service await minglarService.rejectHostApplicationAM(hostXid, userInfo.id); const hostDetails = await minglarService.getUserDetails(hostXid) - await sendAMRejectionMailtoHost(hostDetails.emailAddress) + await sendAMRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName) return { statusCode: 200, diff --git a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts index 30a8e24..1272623 100644 --- a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts +++ b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts @@ -4,6 +4,7 @@ import config from "../../../config/config"; export async function sendEmailToHostForApprovedApplication( emailAddress: string, + name: string ): Promise<{ sent: boolean; // messageId: string @@ -12,7 +13,7 @@ export async function sendEmailToHostForApprovedApplication( const subject = "Approval for your application"; const htmlContent = ` -

Dear Host,

+

Dear ${name},

Congratulations, Your application to minglar admin has been approved.

You can start onboarding your activities through the host panel.

You can login to your account using the link below:
diff --git a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts index 62ddb32..6cec2f1 100644 --- a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts +++ b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts @@ -39,6 +39,7 @@ export async function sendEmailToHostForRejectedApplication( export async function sendAMRejectionMailtoHost( emailAddress: string, + name: string ): Promise<{ sent: boolean; // messageId: string @@ -47,7 +48,7 @@ export async function sendAMRejectionMailtoHost( const subject = "Improvement of your application"; const htmlContent = ` -

Dear Host,

+

Dear ${name},

Your account manager has reviewed your application and provided some suggestions.
Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.

You may access your application using the link below:
From b8f5f92c9896238118c67db7b26fd00f17beb9e2 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 9 Dec 2025 17:38:44 +0530 Subject: [PATCH 04/24] Add validation for city ID in company details submission to ensure existence in the database --- .../Host_Admin/onboarding/submitCompanyDetails.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts index de59512..513d982 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts @@ -387,6 +387,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } } + if (parsedCompany.cityXid) { + const city = await prismaClient.cities.findUnique({ + where: { id: Number(parsedCompany.cityXid) } + }); + if (!city) { + throw new ApiError(400, `City with ID ${parsedCompany.cityXid} not found`); + } + } + /** 12) SAVE / UPDATE HOST ENTRY */ const createdOrUpdated = await hostService.addOrUpdateCompanyDetails( userInfo.id, From 82340c2918db30554ed65f121c399f93cb594e68 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Wed, 10 Dec 2025 14:59:16 +0530 Subject: [PATCH 05/24] Implement email notifications for host application acceptance and rejection, including host details retrieval and email content customization. --- .../handlers/hosthub/hosts/acceptPQByAM.ts | 4 ++ .../handlers/hosthub/hosts/rejectPQQbyAM.ts | 4 ++ .../services/approvalMailtoHost.service.ts | 38 +++++++++++++++ .../minglaradmin/services/minglar.service.ts | 7 +++ .../services/rejectionMailtoHost.service.ts | 46 +++++++++++++++++++ 5 files changed, 99 insertions(+) diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts index f5ff82a..d447556 100644 --- a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts +++ b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts @@ -4,6 +4,7 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import ApiError from '../../../../../common/utils/helper/ApiError'; +import { sendAMPQQAcceptanceMailtoHost } from '../../../../minglaradmin/services/approvalMailtoHost.service'; const minglarService = new MinglarService(prismaClient); @@ -39,6 +40,9 @@ export const handler = safeHandler(async ( Number(activityId), Number(userInfo.id) ); + const hostXid = await minglarService.getHostXidByActivityId(activityId) + const hostDetails = await minglarService.getUserDetails(hostXid) + await sendAMPQQAcceptanceMailtoHost(hostDetails.emailAddress, hostDetails.firstName) return { statusCode: 201, diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts index 322c1f8..38e4886 100644 --- a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts +++ b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts @@ -4,6 +4,7 @@ import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/a import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import ApiError from '../../../../../common/utils/helper/ApiError'; import { MinglarService } from '../../../services/minglar.service'; +import { sendAMPQQRejectionMailtoHost } from '../../../../minglaradmin/services/rejectionMailtoHost.service'; const minglarService = new MinglarService(prismaClient); @@ -39,6 +40,9 @@ export const handler = safeHandler(async ( Number(activityId), Number(userInfo.id) ); + const hostXid = await minglarService.getHostXidByActivityId(activityId) + const hostDetails = await minglarService.getUserDetails(hostXid) + await sendAMPQQRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName) return { statusCode: 201, diff --git a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts index 1272623..06a169a 100644 --- a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts +++ b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts @@ -74,3 +74,41 @@ export async function sendEmailToHostForMinglarApproval( throw new ApiError(500, "Failed to send OTP to minglar admin via email."); } } + +export async function sendAMPQQAcceptanceMailtoHost( + emailAddress: string, + name: string +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Approval for your activity onboarding application"; + + const htmlContent = ` +

Dear ${name},

+

Congratulations, Your activity onboarding application to minglar admin has been approved.

+

You can start adding other details of your activity through the host panel.

+

You can login to your account using the link below:
+ Link: ${config.HOST_LINK}

+

Best regards,
Minglar Team

+ `; + + try { + const result = await brevoService.sendEmail({ + recipients: [{ email: emailAddress }], + subject, + htmlContent, + }); + + console.log("📧 Email sent successfully:", result); + + return { + sent: true, + // messageId: result.messageId + }; + } catch (err) { + console.error("Brevo email send failed:", err); + throw new ApiError(500, "Failed to send OTP to minglar admin via email."); + } +} diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index 19f1153..2623f2a 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -136,6 +136,13 @@ export class MinglarService { return this.prisma.user.findUnique({ where: { emailAddress: email } }); } + async getHostXidByActivityId(activityId: number) { + const activityDetails = await this.prisma.activities.findFirst({ + where: { id: activityId } + }) + return activityDetails.hostXid; + } + async getUserDetails(id: number) { const hostDetail = await this.prisma.hostHeader.findFirst({ where: { id: id } diff --git a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts index 6cec2f1..6170b41 100644 --- a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts +++ b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts @@ -76,3 +76,49 @@ export async function sendAMRejectionMailtoHost( throw new ApiError(500, "Failed to send OTP to minglar admin via email."); } } + + +export async function sendAMPQQRejectionMailtoHost( + emailAddress: string, + name: string +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "Improvement of your activity onboarding application"; + + const htmlContent = ` +

Dear ${name},

+ +

Your account manager has reviewed your activity application and provided some suggestions.
+ Please make the necessary improvements and re-submit your activity application along with the pre-qualification answers to proceed with the onboarding process on Minglar.

+ +

You may access your activity onboarding application using the link below:
+ Link: ${config.HOST_LINK}

+ +

If you have any questions, please feel free to contact the Minglar Support Team.

+ +

Best regards,
+ Minglar Team

+ + `; + + try { + const result = await brevoService.sendEmail({ + recipients: [{ email: emailAddress }], + subject, + htmlContent, + }); + + console.log("📧 Email sent successfully:", result); + + return { + sent: true, + // messageId: result.messageId + }; + } catch (err) { + console.error("Brevo email send failed:", err); + throw new ApiError(500, "Failed to send OTP to minglar admin via email."); + } +} From b5304b3c264144aaef6dc23a25278f4a38b41b10 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Mon, 15 Dec 2025 16:53:12 +0530 Subject: [PATCH 06/24] Refactor OTP email content for host registration and resend functionality to improve clarity and tone. --- src/modules/host/services/resendOTPEmail.service.ts | 4 ++-- src/modules/host/services/sendOTPEmail.service.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/host/services/resendOTPEmail.service.ts b/src/modules/host/services/resendOTPEmail.service.ts index f7a2a34..1561e7f 100644 --- a/src/modules/host/services/resendOTPEmail.service.ts +++ b/src/modules/host/services/resendOTPEmail.service.ts @@ -15,8 +15,8 @@ export async function resendOtpEmail( const htmlContent = `

Dear ${role},

Your new OTP is: ${otp}

-

This code is valid for 5 minutes. Please do not share it with anyone.

-

Best regards,
Minglar Team

+

This code will be valid for the next 5 minutes.

+

Warm regards,
Minglar Team

`; try { diff --git a/src/modules/host/services/sendOTPEmail.service.ts b/src/modules/host/services/sendOTPEmail.service.ts index 15c1e70..e1d9b1f 100644 --- a/src/modules/host/services/sendOTPEmail.service.ts +++ b/src/modules/host/services/sendOTPEmail.service.ts @@ -13,9 +13,10 @@ export async function sendOtpEmailForHost( const htmlContent = `

Dear Host,

-

Your OTP for registration is: ${otp}

-

This code is valid for 5 minutes. Please do not share it with anyone.

-

Best regards,
Minglar Team

+

You’re almost all set! 🎉

+

Enter ${otp} to wrap your registration.

+

This code will be valid for the next 5 minutes.

+

Warm regards,
Minglar Team

`; try { From 0da18b18f71238b4bdd03c77ad7680609fdc5393 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 12:07:42 +0530 Subject: [PATCH 07/24] Add getSuggestionsForAM function and corresponding handler for retrieving suggestions based on host assignments. Update serverless configuration to include new API endpoint. --- serverless/functions/minglaradmin.yml | 17 +++++++ .../hosthub/onboarding/showSuggestionToAM.ts | 44 +++++++++++++++++++ .../minglaradmin/services/minglar.service.ts | 19 ++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts diff --git a/serverless/functions/minglaradmin.yml b/serverless/functions/minglaradmin.yml index c660469..33ed1ba 100644 --- a/serverless/functions/minglaradmin.yml +++ b/serverless/functions/minglaradmin.yml @@ -406,3 +406,20 @@ getAllPQPDetailsForAM: - httpApi: path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid} method: get + + +getSuggestionsForAM: + handler: src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM.handler + memorySize: 384 + package: + patterns: + - 'src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM**' + - 'src/modules/minglaradmin/services/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid} + method: get diff --git a/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts new file mode 100644 index 0000000..7202228 --- /dev/null +++ b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts @@ -0,0 +1,44 @@ +import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../../common/utils/helper/ApiError'; +import { MinglarService } from '../../../../minglaradmin/services/minglar.service'; + +const minglarService = new MinglarService(prismaClient); + +/** + * Get suggestions handler + * Retrieves suggestions based on user's role and host assignments + */ +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Verify authentication token + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError(401, 'This is a protected route. Please provide a valid token.'); + } + + // Verify token and get user info + await verifyMinglarAdminToken(token); + + const hostXid = Number(event.pathParameters?.hostXid) + + // Get suggestions using service + const suggestions = await minglarService.getSuggestionsForAM(hostXid); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Suggestions retrieved successfully', + data: suggestions, + }), + }; +}); diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index 2623f2a..973f12e 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -1423,6 +1423,25 @@ export class MinglarService { return suggestions; } + async getSuggestionsForAM(hostXid: number) { + const suggestions = await this.prisma.hostSuggestion.findMany({ + where: { hostXid: hostXid, isreviewed: false, isActive: true }, + select: { + id: true, + title: true, + comments: true, + isparent: true, + isreviewed: true, + reviewOn: true, + }, + orderBy: { + id: 'asc', + }, + }); + + return suggestions; + } + async acceptHostApplication(host_xid: number, user_xid: number) { return await this.prisma.$transaction(async (tx) => { await this.prisma.hostHeader.update({ From 43e494780d95d57dd489a312352cbe95f37d1ea9 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 13:16:22 +0530 Subject: [PATCH 08/24] Refactor document name handling in host service to sanitize input and ensure valid connections for city, state, and country IDs during host creation and updates. --- src/modules/host/services/host.service.ts | 72 ++++++++++++++++------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index d7f269e..405c68e 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -32,6 +32,15 @@ import { import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import config from '../../../config/config'; +function sanitizeDocumentName(name?: string) { + if (!name) return null; + + return name + .replace(/[^a-zA-Z0-9 _-]/g, '') // remove / . + .substring(0, 100); +} + + type HostCompanyDetailsInput = z.infer; // Document input after S3 upload (with S3 URL as filePath) @@ -629,6 +638,7 @@ export class HostService { where: { userXid: user_xid }, include: { hostParent: true }, }); + console.log(existingHostCompany, "-: Existing hai") let existingParentCompany; @@ -704,12 +714,14 @@ export class HostService { // ------------------------------------------------------- if (!existingHostCompany) { if (!isDraft) { + console.log("First time direct final submit.") const existingByPan = await tx.hostHeader.findFirst({ where: { panNumber: companyData.panNumber }, }); if (existingByPan) throw new ApiError(400, 'Company already exists with this pan/bin number'); } + console.log("First Time Aaya hai") const createdHost = await tx.hostHeader.create({ data: { @@ -721,7 +733,7 @@ export class HostService { states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, pinCode: companyData.pinCode, - logoPath: companyData.logoPath || existingHostCompany.logoPath, + logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, @@ -751,7 +763,7 @@ export class HostService { const docsData = documents.map((doc) => ({ hostXid: createdHost.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, + documentName: sanitizeDocumentName(doc.documentName), filePath: doc.filePath, })); await tx.hostDocuments.createMany({ data: docsData }); @@ -759,23 +771,29 @@ export class HostService { // parent create if (companyData.isSubsidairy && parentCompanyData) { + console.log("Parent ke saath aaya hai first time.") const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: createdHost.id } }, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } + // Safely handle city connection - only connect if valid ID exists + cities: companyData.cityXid && !isNaN(Number(companyData.cityXid)) + ? { connect: { id: Number(companyData.cityXid) } } + : undefined, // Don't change if not provided + + // Same for state + states: companyData.stateXid && !isNaN(Number(companyData.stateXid)) + ? { connect: { id: Number(companyData.stateXid) } } : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } - : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } + + // Same for country + countries: companyData.countryXid && !isNaN(Number(companyData.countryXid)) + ? { connect: { id: Number(companyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath, + logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, @@ -798,7 +816,7 @@ export class HostService { const parentDocsData = parentDocuments.map((doc) => ({ hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, + documentName: sanitizeDocumentName(doc.documentName), filePath: doc.filePath, })); await tx.hostParenetDocuments.createMany({ data: parentDocsData }); @@ -827,9 +845,20 @@ export class HostService { companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, - cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, - states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, - countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, + // Safely handle city connection - only connect if valid ID exists + cities: companyData.cityXid && !isNaN(Number(companyData.cityXid)) + ? { connect: { id: Number(companyData.cityXid) } } + : undefined, // Don't change if not provided + + // Same for state + states: companyData.stateXid && !isNaN(Number(companyData.stateXid)) + ? { connect: { id: Number(companyData.stateXid) } } + : undefined, + + // Same for country + countries: companyData.countryXid && !isNaN(Number(companyData.countryXid)) + ? { connect: { id: Number(companyData.countryXid) } } + : undefined, pinCode: companyData.pinCode, logoPath: companyData.logoPath || existingHostCompany.logoPath, isSubsidairy: companyData.isSubsidairy, @@ -859,6 +888,7 @@ export class HostService { // documents UPSERT if (documents?.length) { for (const doc of documents) { + if (!doc.filePath) continue; const existingDoc = await tx.hostDocuments.findFirst({ where: { hostXid: updatedHost.id, @@ -871,7 +901,7 @@ export class HostService { where: { id: existingDoc.id }, data: { filePath: doc.filePath, - documentName: doc.documentName || existingDoc.documentName, + documentName: sanitizeDocumentName(doc.documentName) || existingDoc.documentName, }, }); } else { @@ -879,7 +909,7 @@ export class HostService { data: { hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, + documentName: sanitizeDocumentName(doc.documentName), filePath: doc.filePath, }, }); @@ -891,8 +921,9 @@ export class HostService { if (companyData.isSubsidairy) { const parentRecords = existingHostCompany.hostParent; const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords; - + console.log("Yaha aaya update in the apretn me") if (!parentRecord) { + console.log("Parent record nahi mila to create kar raha hai.") const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: updatedHost.id } }, @@ -933,7 +964,7 @@ export class HostService { data: { hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, + documentName: sanitizeDocumentName(doc.documentName), filePath: doc.filePath, }, }); @@ -988,7 +1019,7 @@ export class HostService { where: { id: existingParentDoc.id }, data: { filePath: doc.filePath, - documentName: doc.documentName || existingParentDoc.documentName, + documentName: sanitizeDocumentName(doc.documentName) || existingParentDoc.documentName, }, }); } else { @@ -996,7 +1027,7 @@ export class HostService { data: { hostParentXid: parentRecord.id, documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, + documentName: sanitizeDocumentName(doc.documentName), filePath: doc.filePath, }, }); @@ -1005,6 +1036,7 @@ export class HostService { } } } else { + console.log("Last ke else block me aaya hai") const previousParent = existingHostCompany.hostParent; let prevParentId = null; From 46daec00ce4e986deea8699bad711482ddc5f9c8 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 14:37:38 +0530 Subject: [PATCH 09/24] fixed the keytooloong issue --- .../onboarding/submitCompanyDetails.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts index 513d982..675770a 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts @@ -17,6 +17,16 @@ import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHo const hostService = new HostService(prismaClient); +function getExtensionFromMime(mimeType: string) { + const map: Record = { + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'application/pdf': 'pdf', + 'image/webp': 'webp', + }; + return map[mimeType] || 'bin'; +} + const s3 = new AWS.S3({ region: config.aws.region, }); @@ -284,11 +294,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } } - - /** 11) UPLOAD DOCUMENTS */ async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) { - const ext = originalName.split('.').pop() || 'jpg'; + // const ext = originalName.split('.').pop() || 'jpg'; + const ext = getExtensionFromMime(mimeType); let s3Key = ''; @@ -362,28 +371,40 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } /** UPLOAD LOGO (if provided) */ - const logoFile = files.find((f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'); - if (logoFile) { - const logoUrl = await uploadToS3(logoFile.buffer, logoFile.mimeType, logoFile.fileName, 'logo'); + const logoFile = files.find( + (f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile' + ); + + if (logoFile && logoFile.buffer && logoFile.fileName) { + const logoUrl = await uploadToS3( + logoFile.buffer, + logoFile.mimeType, + logoFile.fileName, + 'logo' + ); parsedCompany.logoPath = logoUrl; } /** UPLOAD PARENT COMPANY LOGO (if provided) */ - const parentLogoFile = files.find((f) => f.fieldName === 'parentCompanyLogo'); - if (parentLogoFile) { + const parentLogoFile = files.find( + (f) => f.fieldName === 'parentCompanyLogo' + ); + + if (parentLogoFile && parentLogoFile.buffer && parentLogoFile.mimeType) { + // 🔒 Only upload when an actual file is present const parentLogoUrl = await uploadToS3( parentLogoFile.buffer, parentLogoFile.mimeType, - parentLogoFile.fileName, + parentLogoFile.fileName, // safe here because it's a real file 'parent_company_logo', ); if (parsedParentCompany) { parsedParentCompany.logoPath = parentLogoUrl; } else { - // if no parent object exists yet (drafts or other flows), attach it safely - parsedParentCompany = parsedParentCompany || {}; - parsedParentCompany.logoPath = parentLogoUrl; + parsedParentCompany = { + logoPath: parentLogoUrl, + }; } } From 2767d29d794eeef7939f95cfcab866137a8c2eee Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 15:38:03 +0530 Subject: [PATCH 10/24] Update parent company validation to allow optional fields and handle null values in company details submission. --- .../validation/host/hostCompanyDetails.validation.ts | 6 +++--- .../Host_Admin/onboarding/submitCompanyDetails.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts index 0012e4e..a6ca41a 100644 --- a/src/common/utils/validation/host/hostCompanyDetails.validation.ts +++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts @@ -2,8 +2,8 @@ import { z } from "zod"; export const parentCompanySchema = z.object({ companyName: z.string() - .min(1, "Parent company name is required") - .max(100, "Parent company name cannot exceed 100 characters"), + .max(100, "Parent company name cannot exceed 100 characters") + .optional(), address1: z.string() .max(150, "Address1 cannot exceed 150 characters") @@ -44,7 +44,7 @@ export const parentCompanySchema = z.object({ }), companyTypeXid: z.number() - .min(1, "Company type XID is required"), + .optional(), websiteUrl: z.string().nullable().optional(), instagramUrl: z.string().nullable().optional(), diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts index 675770a..40491aa 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts @@ -159,6 +159,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } } + if ( + companyDetailsRaw.parentCompany && + Object.values(companyDetailsRaw.parentCompany).every( + (v) => v === undefined || v === null + ) + ) { + companyDetailsRaw.parentCompany = null; + } + /** 6) Profile update if provided */ if (fields.userProfile) { const userProfileRaw = normalizeJsonField(fields, 'userProfile'); From ef2b23ef83d16f2419d366af94c6936fe3f2ed0f Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Tue, 16 Dec 2025 16:07:03 +0530 Subject: [PATCH 11/24] Update serverless configuration to use exported stack output for Prisma Lambda Layer ARN, ensuring proper deployment functionality. --- serverless.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/serverless.yml b/serverless.yml index 9a388ab..a1ec7ae 100644 --- a/serverless.yml +++ b/serverless.yml @@ -20,7 +20,8 @@ provider: # Apply Prisma layer to all functions # Reference the layer defined in this stack using CloudFormation Ref layers: - - !Ref PrismaLambdaLayer + # Use the exported stack output so deploy function works (expects a string ARN) + - ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn} apiGateway: binaryMediaTypes: - '*/*' From a2907929d4fdb1611759d45fd6d0831d1e9371ed Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 16:08:59 +0530 Subject: [PATCH 12/24] updated serverless version --- package-lock.json | 10 +++++----- package.json | 2 +- serverless.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23ef980..584578f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "prisma": "^7.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "serverless": "4.17.0", + "serverless": "4.24.0", "swagger-ui-express": "^5.0.0", "tslib": "^2.8.1", "uuid": "^13.0.0", @@ -14467,12 +14467,12 @@ } }, "node_modules/serverless": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/serverless/-/serverless-4.17.0.tgz", - "integrity": "sha512-hoZmipwyN/h7y9HwkWGlJ0YT06RFq7WNOD7fFEiPfnSnnUMVTzeNHq2BRrUlpHhf5s9srCHDc2wx5I06acfq1Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/serverless/-/serverless-4.24.0.tgz", + "integrity": "sha512-bgxFQ6QyOGJC9IZjZIXo4m6bdWMl9I7HNZ4jrmwSpdePdsRd46igGRpSnhdYFOc71GNplhSOeoCibL94yCHfrg==", "hasInstallScript": true, "dependencies": { - "axios": "^1.8.3", + "axios": "^1.12.1", "axios-proxy-builder": "^0.1.2", "rimraf": "^5.0.5", "xml2js": "0.6.2" diff --git a/package.json b/package.json index 12e888a..bc02f79 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "prisma": "^7.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "serverless": "4.17.0", + "serverless": "4.24.0", "swagger-ui-express": "^5.0.0", "tslib": "^2.8.1", "uuid": "^13.0.0", diff --git a/serverless.yml b/serverless.yml index 9a388ab..5f913d2 100644 --- a/serverless.yml +++ b/serverless.yml @@ -20,7 +20,7 @@ provider: # Apply Prisma layer to all functions # Reference the layer defined in this stack using CloudFormation Ref layers: - - !Ref PrismaLambdaLayer + - { Ref: PrismaLambdaLayer } apiGateway: binaryMediaTypes: - '*/*' From 4d3796c5f3dcdf56c85ee268eb45c0ab564bf0b3 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 16:36:15 +0530 Subject: [PATCH 13/24] Refactor companyTypes seeding and enhance host retrieval to include user email address --- prisma/seed.ts | 10 ++++++++-- src/modules/host/handlers/getStepper.ts | 1 + src/modules/host/services/host.service.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 291cd0c..71eaf9f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -250,9 +250,9 @@ async function main() { }); await prisma.companyTypes.upsert({ - where: { companyTypeName: 'Private Limited, Public Limited' }, + where: { companyTypeName: 'Private Limited' }, update: {}, - create: { companyTypeName: 'Private Limited, Public Limited', displayOrder: 5 }, + create: { companyTypeName: 'Private Limited', displayOrder: 5 }, }); await prisma.companyTypes.upsert({ @@ -260,6 +260,12 @@ async function main() { update: {}, create: { companyTypeName: 'Non-Profit Organisation', displayOrder: 6 }, }); + + await prisma.companyTypes.upsert({ + where: { companyTypeName: 'Public Limited' }, + update: {}, + create: { companyTypeName: 'Public Limited', displayOrder: 7 }, + }); // ✅ Food Types await prisma.foodTypes.createMany({ diff --git a/src/modules/host/handlers/getStepper.ts b/src/modules/host/handlers/getStepper.ts index 38e3b63..45c00f9 100644 --- a/src/modules/host/handlers/getStepper.ts +++ b/src/modules/host/handlers/getStepper.ts @@ -42,6 +42,7 @@ export const handler = safeHandler(async ( message: 'Stepper information retrieved successfully', data: { stepper: host.stepper, + emailAddress: host.user?.emailAddress || null, }, }), }; diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 405c68e..e7c6929 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -85,7 +85,7 @@ export class HostService { async getHostIdByUserXid(user_xid: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, - select: { id: true, companyName: true, countryXid: true, stepper: true }, + select: { id: true, companyName: true, countryXid: true, stepper: true, user: { select: { id: true, emailAddress: true }} }, }); return host; } From fab76423025131e0fb1f935224d91055cc3b2d93 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 17:34:10 +0530 Subject: [PATCH 14/24] Implement parent document deletion and improve host retrieval logic in onboarding process --- .../onboarding/submitCompanyDetails.ts | 15 +++ src/modules/host/handlers/getStepper.ts | 6 +- src/modules/host/services/host.service.ts | 107 +++++++++++++----- 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts index 40491aa..c3b8493 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts @@ -426,6 +426,21 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } } + if (!parsedCompany.isSubsidairy) { + const parentDocuments = await hostService.getParentDocumentsByHostId(userInfo.id); + if (parentDocuments.length > 0) { + for (const doc of parentDocuments) { + try { + const s3Key = getS3KeyFromUrl(doc.filePath); + await deleteFromS3(s3Key); + } catch (e) { + console.error("S3 delete failed:", doc.filePath, e); + } + } + } + await hostService.deleteExistingParentRecords(userInfo.id) + } + /** 12) SAVE / UPDATE HOST ENTRY */ const createdOrUpdated = await hostService.addOrUpdateCompanyDetails( userInfo.id, diff --git a/src/modules/host/handlers/getStepper.ts b/src/modules/host/handlers/getStepper.ts index 45c00f9..8f9bc19 100644 --- a/src/modules/host/handlers/getStepper.ts +++ b/src/modules/host/handlers/getStepper.ts @@ -27,10 +27,6 @@ export const handler = safeHandler(async ( // Fetch user with their HostHeader stepper info const host = await hostService.getHostIdByUserXid(userId); - if (!host) { - throw new ApiError(404, 'Host record not found'); - } - return { statusCode: 200, headers: { @@ -41,7 +37,7 @@ export const handler = safeHandler(async ( success: true, message: 'Stepper information retrieved successfully', data: { - stepper: host.stepper, + stepper: host?.host?.stepper || null, emailAddress: host.user?.emailAddress || null, }, }), diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index e7c6929..4322178 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -85,9 +85,14 @@ export class HostService { async getHostIdByUserXid(user_xid: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, - select: { id: true, companyName: true, countryXid: true, stepper: true, user: { select: { id: true, emailAddress: true }} }, + select: { id: true, stepper: true }, }); - return host; + + const user = await this.prisma.user.findUnique({ + where: { id: user_xid }, + select: { id: true, emailAddress: true }, + }) + return { host, user }; } async getHostById(id: number) { @@ -624,6 +629,52 @@ export class HostService { }); } + async getParentDocumentsByHostId(userId: number) { + const host = await this.prisma.hostHeader.findFirst({ + where: { userXid: userId }, + select: { id: true }, + }); + + if (!host) return []; + + const parents = await this.prisma.hostParent.findMany({ + where: { hostXid: host.id }, + include: { HostParenetDocuments: true }, + }); + + return parents.flatMap(p => p.HostParenetDocuments); + } + + + async deleteExistingParentRecords(userId: number) { + const host = await this.prisma.hostHeader.findFirst({ + where: { userXid: userId }, + select: { id: true }, + }); + + if (!host) return; + + const parents = await this.prisma.hostParent.findMany({ + where: { hostXid: host.id }, + select: { id: true }, + }); + + if (!parents.length) return; + + const parentIds = parents.map(p => p.id); + + // 1️⃣ Delete documents first + await this.prisma.hostParenetDocuments.deleteMany({ + where: { hostParentXid: { in: parentIds } }, + }); + + // 2️⃣ Then delete parent records + await this.prisma.hostParent.deleteMany({ + where: { id: { in: parentIds } }, + }); + } + + async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, @@ -775,22 +826,20 @@ export class HostService { const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: createdHost.id } }, - companyName: parentCompanyData.companyName, + companyName: parentCompanyData.companyName || null, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, // Safely handle city connection - only connect if valid ID exists - cities: companyData.cityXid && !isNaN(Number(companyData.cityXid)) - ? { connect: { id: Number(companyData.cityXid) } } - : undefined, // Don't change if not provided - - // Same for state - states: companyData.stateXid && !isNaN(Number(companyData.stateXid)) - ? { connect: { id: Number(companyData.stateXid) } } + cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } : undefined, - // Same for country - countries: companyData.countryXid && !isNaN(Number(companyData.countryXid)) - ? { connect: { id: Number(companyData.countryXid) } } + states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } + : undefined, + + countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, @@ -927,17 +976,19 @@ export class HostService { const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: updatedHost.id } }, - companyName: parentCompanyData.companyName, + companyName: parentCompanyData.companyName || null, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } + cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } + + states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } + + countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath, @@ -974,17 +1025,19 @@ export class HostService { await tx.hostParent.update({ where: { id: parentRecord.id }, data: { - companyName: parentCompanyData.companyName, + companyName: parentCompanyData.companyName || null, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, - cities: parentCompanyData.cityXid - ? { connect: { id: parentCompanyData.cityXid } } + cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } : undefined, - states: parentCompanyData.stateXid - ? { connect: { id: parentCompanyData.stateXid } } + + states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } : undefined, - countries: parentCompanyData.countryXid - ? { connect: { id: parentCompanyData.countryXid } } + + countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath, From 8ec8cf485463ad4c7a8be8e5159d732b49cb52bc Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 16 Dec 2025 19:11:15 +0530 Subject: [PATCH 15/24] fixed the path --- serverless/functions/minglaradmin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serverless/functions/minglaradmin.yml b/serverless/functions/minglaradmin.yml index 33ed1ba..c718cd8 100644 --- a/serverless/functions/minglaradmin.yml +++ b/serverless/functions/minglaradmin.yml @@ -409,7 +409,7 @@ getAllPQPDetailsForAM: getSuggestionsForAM: - handler: src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM.handler + handler: src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.handler memorySize: 384 package: patterns: From a906dc56359ca0357fef5618b23b1d31f642bbb4 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Wed, 17 Dec 2025 16:17:55 +0530 Subject: [PATCH 16/24] Add getAddActivityPrePopulate handler and implement prepopulate data retrieval - Introduced a new handler for adding activity prepopulation. - Enhanced the PrePopulateService to fetch all necessary prepopulate data for the new activity. - Updated the updateBankDetails handler to correctly inject host ID. - Improved user data retrieval in HostService to include additional fields. - Added validation for host ID in showSuggestionToAM handler. --- serverless/functions/prepopulate.yml | 17 +++++- .../onboarding/updateBankDetails.ts | 2 +- src/modules/host/services/host.service.ts | 9 ++- .../hosthub/onboarding/showSuggestionToAM.ts | 7 +++ .../handlers/getAddActivityPrePopulate.ts | 38 ++++++++++++ .../services/prepopulate.service.ts | 58 +++++++++++++++++++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts diff --git a/serverless/functions/prepopulate.yml b/serverless/functions/prepopulate.yml index 7521ddb..d9282b5 100644 --- a/serverless/functions/prepopulate.yml +++ b/serverless/functions/prepopulate.yml @@ -91,4 +91,19 @@ getFrequenciesOfActivity: events: - httpApi: path: /prepopulate/get-all-Frequencies - method: get \ No newline at end of file + method: get + +getAddActivityPrePopulate: + handler: src/modules/prepopulate/handlers/getAddActivityPrePopulate.handler + memorySize: 384 + package: + patterns: + - 'src/modules/prepopulate/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /prepopulate/get-add-activity-prepopulate + method: get diff --git a/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts index edfabfe..0853bd7 100644 --- a/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts +++ b/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts @@ -43,7 +43,7 @@ export const handler = safeHandler(async ( // ✅ Validate payload using Zod const validationResult = hostBankDetailsSchema.safeParse({ ...(body as object), - hostXid: host.id, // inject hostId from token (not from user input) + hostXid: host.host.id, // inject hostId from token (not from user input) }); if (!validationResult.success) { diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 4322178..81b6b11 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -308,11 +308,14 @@ export class HostService { select: { id: true, roleXid: true, + firstName: true, + lastName: true, + emailAddress: true, + mobileNumber: true, userPassword: true, userStatus: true } }); - console.log(existingUser, "ajsbfkjd") if (!existingUser) { throw new ApiError(404, 'User not found'); @@ -991,7 +994,7 @@ export class HostService { ? { connect: { id: Number(parentCompanyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath, + logoPath: parentCompanyData?.logoPath || existingParentCompany?.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, @@ -1040,7 +1043,7 @@ export class HostService { ? { connect: { id: Number(parentCompanyData.countryXid) } } : undefined, pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath, + logoPath: parentCompanyData?.logoPath || existingParentCompany?.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, diff --git a/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts index 7202228..dd83b3d 100644 --- a/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts +++ b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts @@ -26,6 +26,13 @@ export const handler = safeHandler(async ( const hostXid = Number(event.pathParameters?.hostXid) + if (!hostXid) { + throw new ApiError( + 400, + 'Host ID is required in path parameters.', + ); + } + // Get suggestions using service const suggestions = await minglarService.getSuggestionsForAM(hostXid); diff --git a/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts b/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts new file mode 100644 index 0000000..c23def5 --- /dev/null +++ b/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts @@ -0,0 +1,38 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { prismaClient } from '../../../common/database/prisma.lambda.service'; +import { verifyMinglarAdminHostToken } from '../../../common/middlewares/jwt/authForMinglarAdminHost'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { PrePopulateService } from '../services/prepopulate.service'; + +const prePopulateService = new PrePopulateService(prismaClient); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Extract token from headers + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'] + if (!token) { + throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); + } + + // Authenticate user using the shared authForHost function + await verifyMinglarAdminHostToken(token); + + const result = await prePopulateService.getAllPrePopulateDataForAddActivity(); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Data retrieved successfully', + data: result, + }), + }; +}); + diff --git a/src/modules/prepopulate/services/prepopulate.service.ts b/src/modules/prepopulate/services/prepopulate.service.ts index b62e2ae..563fb85 100644 --- a/src/modules/prepopulate/services/prepopulate.service.ts +++ b/src/modules/prepopulate/services/prepopulate.service.ts @@ -141,4 +141,62 @@ export class PrePopulateService { }, }); } + + async getAllPrePopulateDataForAddActivity() { + const [ + foodType, + cuisineDetails, + vehicleType, + navigationMode, + taxDetails, + energyLevel, + aminitiesDetails, + allowedEntryType, + ageRestrictionDetails + ] = + await this.prisma.$transaction([ + this.prisma.foodTypes.findMany({ + where: { isActive: true }, + orderBy: { foodTypeName: 'asc' }, + }), + this.prisma.foodCuisines.findMany({ + where: { isActive: true }, + }), + this.prisma.transportModes.findMany({ + where: { isActive: true }, + }), + this.prisma.navigationModes.findMany({ + where: { isActive: true }, + }), + this.prisma.taxes.findMany({ + where: { isActive: true }, + }), + this.prisma.energyLevels.findMany({ + where: { isActive: true }, + }), + this.prisma.amenities.findMany({ + where: { isActive: true }, + }), + this.prisma.allowedEntryTypes.findMany({ + where: { isActive: true }, + orderBy: { allowedEntryTypeName: 'asc' } + }), + this.prisma.ageRestrictions.findMany({ + where: { isActive: true }, + orderBy: { ageRestrictionName: 'asc' } + }), + ]); + + return { + foodType, + cuisineDetails, + vehicleType, + navigationMode, + taxDetails, + energyLevel, + aminitiesDetails, + allowedEntryType, + ageRestrictionDetails + }; + } } From 05e48063c928b66a11cda9f673f934eb988d4d8f Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Wed, 17 Dec 2025 16:57:18 +0530 Subject: [PATCH 17/24] Add isActive check when retrieving user in loginForHost method --- src/modules/host/services/host.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 81b6b11..cf4449e 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -304,7 +304,7 @@ export class HostService { async loginForHost(emailAddress: string, userPassword: string) { const existingUser = await this.prisma.user.findUnique({ - where: { emailAddress: emailAddress }, + where: { emailAddress: emailAddress, isActive: true }, select: { id: true, roleXid: true, From 91871d1f4428b09923bd5ce0bb6837dd8e8b8005 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Wed, 17 Dec 2025 17:29:17 +0530 Subject: [PATCH 18/24] Enhance search filter to include company name and user reference number in host retrieval --- .../minglaradmin/services/minglar.service.ts | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index 973f12e..6608294 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -748,21 +748,60 @@ export class MinglarService { }; /** SEARCH FILTER **/ + // if (search?.trim()) { + // const term = search.trim(); + + // if (/^\d+$/.test(term)) { + // filters.id = Number(term); + // } else { + // filters.user = { + // ...filters.user, + // OR: [ + // { emailAddress: { contains: term, mode: 'insensitive' } }, + // { firstName: { contains: term, mode: 'insensitive' } }, + // { lastName: { contains: term, mode: 'insensitive' } }, + // ], + // }; + // } + // } if (search?.trim()) { const term = search.trim(); - - if (/^\d+$/.test(term)) { - filters.id = Number(term); - } else { - filters.user = { - ...filters.user, + filters.AND = [ + { OR: [ - { emailAddress: { contains: term, mode: 'insensitive' } }, - { firstName: { contains: term, mode: 'insensitive' } }, - { lastName: { contains: term, mode: 'insensitive' } }, + { + companyName: { + contains: term, + mode: 'insensitive', + }, + }, + { + user: { + OR: [ + { + firstName: { + contains: term, + mode: 'insensitive', + }, + }, + { + lastName: { + contains: term, + mode: 'insensitive', + }, + }, + { + userRefNumber: { + contains: term, + mode: 'insensitive', + }, + }, + ], + }, + }, ], - }; - } + }, + ]; } /** USER STATUS FILTER **/ From c9b507f96932dae327894aee6e8a50511c846f54 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Thu, 18 Dec 2025 16:55:57 +0530 Subject: [PATCH 19/24] Enhance rejection email content with clickable link for application access --- prisma/schema.prisma | 248 +++++++++++------- .../services/rejectionMailtoHost.service.ts | 6 +- 2 files changed, 156 insertions(+), 98 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4db51d4..e902b9c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -67,6 +67,11 @@ model User { userAddressDetails UserAddressDetails[] userDocuments UserDocuments[] activityTracks ActivityTrack[] + // 🔹 Activities created by this user + createdActivities Activities[] @relation("UserActivities") + + // 🔹 Activities where this user is Account Manager + managedActivities Activities[] @relation("ActivityAccountManager") @@map("users") @@schema("usr") @@ -394,12 +399,13 @@ model DocumentType { } model FoodCuisines { - id Int @id @default(autoincrement()) - cuisineName String @unique @map("cuisine_name") @db.VarChar(30) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + cuisineName String @unique @map("cuisine_name") @db.VarChar(30) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + activityCuisines ActivityCuisine[] @@map("food_cuisines") @@schema("mst") @@ -434,13 +440,14 @@ model Amenities { } model FoodTypes { - id Int @id @default(autoincrement()) - foodTypeName String @unique @map("food_type_name") @db.VarChar(30) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - ActivityFoodDetails ActivityFoodDetails[] + id Int @id @default(autoincrement()) + foodTypeName String @unique @map("food_type_name") @db.VarChar(30) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + ActivityFoodCost ActivityFoodCost[] + activityFoodTypes ActivityFoodTypes[] @@map("food_types") @@schema("mst") @@ -614,6 +621,7 @@ model EnergyLevels { id Int @id @default(autoincrement()) energyLevelName String @map("energy_level_name") @db.VarChar(30) energyIcon String @map("energy_icon") @db.VarChar(400) + energyColor String @map("energy_color") @db.VarChar(20) isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -844,38 +852,45 @@ model HostTrack { // ACTIVITY MODELS model Activities { - id Int @id @default(autoincrement()) - hostXid Int @map("host_xid") - host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) - activityTypeXid Int @map("activity_type_xid") - activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict) - frequenciesXid Int? @map("frequencies_xid") - frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict) - activityRefNumber String? @map("activity_ref_number") @db.VarChar(30) - activityTitle String? @map("activity_title") @db.VarChar(30) - activityDescription String? @map("activity_description") @db.VarChar(80) - checkInLat Float? @map("check_in_lat") - checkInLong Float? @map("check_in_long") - checkInAddress String? @map("check_in_address") @db.VarChar(150) - isCheckOutSame Boolean? @default(true) @map("is_check_out_same") - checkOutLat Float? @map("check_out_lat") - checkOutLong Float? @map("check_out_long") - checkOutAddress String? @map("check_out_address") @db.VarChar(150) - energyLevelXid Int? @map("energy_level_xid") - energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict) - activityDurationMins Int? @map("activity_duration_mins") - foodAvailable Boolean? @default(false) @map("food_available") - foodIsChargeable Boolean? @default(false) @map("food_is_chargeable") - alcoholAvailable Boolean? @default(false) @map("alcohol_available") - trainerAvailable Boolean? @default(false) @map("trainer_available") - trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable") - pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available") - pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable") - inActivityAvailable Boolean? @default(false) @map("in_activity_available") - inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable") - equipmentAvailable Boolean? @default(false) @map("equipment_available") - equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable") - cancellationAvailable Boolean? @default(false) @map("cancellation_available") + id Int @id @default(autoincrement()) + hostXid Int @map("host_xid") + host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) + activityTypeXid Int @map("activity_type_xid") + activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict) + frequenciesXid Int? @map("frequencies_xid") + frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict) + activityRefNumber String? @map("activity_ref_number") @db.VarChar(30) + activityTitle String? @map("activity_title") @db.VarChar(30) + activityDescription String? @map("activity_description") @db.VarChar(80) + checkInLat Float? @map("check_in_lat") + checkInLong Float? @map("check_in_long") + checkInAddress String? @map("check_in_address") @db.VarChar(150) + isCheckOutSame Boolean? @default(true) @map("is_check_out_same") + checkOutLat Float? @map("check_out_lat") + checkOutLong Float? @map("check_out_long") + checkOutAddress String? @map("check_out_address") @db.VarChar(150) + energyLevelXid Int? @map("energy_level_xid") + energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict) + activityDurationMins Int? @map("activity_duration_mins") + foodAvailable Boolean? @default(false) @map("food_available") + foodIsChargeable Boolean? @default(false) @map("food_is_chargeable") + alcoholAvailable Boolean? @default(false) @map("alcohol_available") + trainerAvailable Boolean? @default(false) @map("trainer_available") + trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable") + pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available") + pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable") + inActivityAvailable Boolean? @default(false) @map("in_activity_available") + inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable") + equipmentAvailable Boolean? @default(false) @map("equipment_available") + equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable") + cancellationAvailable Boolean? @default(false) @map("cancellation_available") + // 🔹 Creator / owner + userId Int? + user User? @relation("UserActivities", fields: [userId], references: [id]) + + // 🔹 Account Manager + accountManagerXid Int? + accountManager User? @relation("ActivityAccountManager", fields: [accountManagerXid], references: [id], onDelete: Restrict) cancellationAllowedBeforeMins Int? @map("cancellation_allowed_before_mins") currencyXid Int? @map("currency_xid") currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict) @@ -902,7 +917,7 @@ model Activities { ActivityVenueArtifacts ActivityVenueArtifacts[] ActivityPQQheader ActivityPQQheader[] ActivityAllowedEntry ActivityAllowedEntry[] - ActivityFoodDetails ActivityFoodDetails[] + ActivityFoodCost ActivityFoodCost[] ActivityEquipments ActivityEquipments[] ActivityNavigationModes ActivityNavigationModes[] ActivityPickUpDetails ActivityPickUpDetails[] @@ -911,6 +926,9 @@ model Activities { ScheduleHeader ScheduleHeader[] ItineraryActivities ItineraryActivities[] activityTracks ActivityTrack[] + activityFoodTypes ActivityFoodTypes[] + activityCuisines ActivityCuisine[] + activityPickUpTransports ActivityPickUpTransport[] @@map("activities") @@schema("act") @@ -920,7 +938,6 @@ model ActivityOtherDetails { id Int @id @default(autoincrement()) activityXid Int @map("activity_xid") activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) - foodCuisines String? @map("food_cuisines") @db.VarChar(30) exclusiveNotes String? @map("exclusive_notes") @db.VarChar(50) dosNotes String? @map("dos_notes") @db.VarChar(200) dontsNotes String? @map("donts_notes") @db.VarChar(200) @@ -1036,10 +1053,14 @@ model ActivityEligibility { weightRestrictionName String? @map("weight_restriction_name") @db.VarChar(30) weightEntered Int? @map("weight_entered") weightIn String? @map("weight_in") @db.VarChar(30) + minWeight Int? @map("min_weight") + maxWeight Int? @map("max_weight") isHeightRestriction Boolean @default(false) @map("is_height_restriction") heightRestrictionName String? @map("height_restriction_name") @db.VarChar(30) heightEntered Int? @map("height_entered") heightIn String? @map("height_in") @db.VarChar(30) + minHeight Int? @map("min_height") + maxHeight Int? @map("max_height") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -1204,12 +1225,10 @@ model ActivityAllowedEntry { @@schema("act") } -model ActivityFoodDetails { +model ActivityFoodCost { id Int @id @default(autoincrement()) activityXid Int @map("activity_xid") activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) - foodTypeXid Int @map("food_type_xid") - foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict) baseAmount Int @map("base_amount") totalAmount Int @map("total_amount") isActive Boolean @default(true) @map("is_active") @@ -1217,23 +1236,55 @@ model ActivityFoodDetails { updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") ActivityFoodTaxes ActivityFoodTaxes[] + foodTypes FoodTypes? @relation(fields: [foodTypesId], references: [id]) + foodTypesId Int? - @@map("activity_food_details") + @@map("activity_food_cost") + @@schema("act") +} + +model ActivityFoodTypes { + id Int @id @default(autoincrement()) + activityXid Int @map("activity_xid") + activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) + foodTypeXid Int @map("food_type_xid") + foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("activity_food_types") + @@schema("act") +} + +model ActivityCuisine { + id Int @id @default(autoincrement()) + activityXid Int @map("activity_xid") + activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) + foodCuisineXid Int @map("food_cuisine_xid") + foodCuisine FoodCuisines @relation(fields: [foodCuisineXid], references: [id], onDelete: Restrict) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("activity_cuisine") @@schema("act") } model ActivityFoodTaxes { - id Int @id @default(autoincrement()) - activityFoodDetailsXid Int @map("activity_food_details_xid") - activityFoodDetails ActivityFoodDetails @relation(fields: [activityFoodDetailsXid], references: [id], onDelete: Cascade) - taxXid Int @map("tax_xid") - taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict) - taxPer Float @map("tax_per") - taxAmount Int @map("tax_amount") - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + activityFoodCostXid Int @map("activity_food_cost_xid") + activityFoodCost ActivityFoodCost @relation(fields: [activityFoodCostXid], references: [id], onDelete: Cascade) + taxXid Int @map("tax_xid") + taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict) + taxPer Float @map("tax_per") + taxAmount Int @map("tax_amount") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("activity_food_taxes") @@schema("act") @@ -1310,54 +1361,57 @@ model ActivityNavigationModesTaxes { } model ActivityPickUpDetails { - id Int @id @default(autoincrement()) - activityXid Int @map("activity_xid") - activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) - isPickUp Boolean @default(false) @map("is_pick_up") - locationLat Float? @map("location_lat") - locationLong Float? @map("location_long") - locationAddress String? @map("location_address") @db.VarChar(150) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - ActivityPickUpTransport ActivityPickUpTransport[] - - @@map("activity_pick_up_details") - @@schema("act") -} - -model ActivityPickUpTransport { id Int @id @default(autoincrement()) - activityPickUpDetailsXid Int @map("activity_pick_up_details_xid") - activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade) - transportModeXid Int @map("transport_mode_xid") - transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict) - isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable") + activityPickUpTransportXid Int @map("activity_pick_up_transport_xid") + activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade) + isPickUp Boolean @default(false) @map("is_pick_up") + locationLat Float? @map("location_lat") + locationLong Float? @map("location_long") + locationAddress String? @map("location_address") @db.VarChar(150) transportBasePrice Int @map("transport_base_price") transportTotalPrice Int @map("transport_total_price") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - ActivityPickUpTransportTaxes ActivityPickUpTransportTaxes[] + activities Activities? @relation(fields: [activitiesId], references: [id]) + activitiesId Int? + activityPickUpTransportTaxes ActivityPickUpTransportTaxes[] + + @@map("activity_pick_up_details") + @@schema("act") +} + +model ActivityPickUpTransport { + id Int @id @default(autoincrement()) + activityXid Int @map("activity_xid") + activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) + transportModeXid Int @map("transport_mode_xid") + transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict) + isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + pickupDetails ActivityPickUpDetails[] @@map("activity_pick_up_transport") @@schema("act") } model ActivityPickUpTransportTaxes { - id Int @id @default(autoincrement()) - activityPickUpTransportXid Int @map("activity_pick_up_transport_xid") - activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade) - taxXid Int @map("tax_xid") - taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict) - taxPer Float @map("tax_per") - taxAmount Int @map("tax_amount") - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + activityPickUpDetailsXid Int @map("activity_pick_up_details_xid") + activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade) + taxXid Int @map("tax_xid") + taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict) + taxPer Float @map("tax_per") + taxAmount Int @map("tax_amount") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("activity_pick_up_transport_taxes") @@schema("act") diff --git a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts index 6170b41..8f50509 100644 --- a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts +++ b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts @@ -52,7 +52,11 @@ export async function sendAMRejectionMailtoHost(

Your account manager has reviewed your application and provided some suggestions.
Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.

You may access your application using the link below:
- Link: ${config.HOST_LINK}

+ Link: + + ${config.HOST_LINK} + +

If you have any questions, please feel free to contact the Minglar Support Team.

Best regards,
Minglar Team

From 53785bd5f21e23d8aef87a86e5d763b29be1da5f Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 18 Dec 2025 19:37:32 +0530 Subject: [PATCH 20/24] Add handler for creating full activity with related records --- .../OnBoarding/CreateNewActivity.ts | 66 +++++ src/modules/host/services/host.service.ts | 272 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts new file mode 100644 index 0000000..59e15a5 --- /dev/null +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -0,0 +1,66 @@ +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../../common/utils/helper/ApiError'; +import { HostService } from '../../../services/host.service'; + +const hostService = new HostService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context, + ): Promise => { + // Verify authentication token + const token = + event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError( + 401, + 'This is a protected route. Please provide a valid token.', + ); + } + + // Verify token and get user info + const userInfo = await verifyHostToken(token); + + let body: any = {}; + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (err) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + // Accept multiple possible keys from the frontend payload + const rawActivityType = body.activityTypeXid ?? body.activityType ?? body.activity_type_xid; + const rawFrequencies = body.frequenciesXid ?? body.frequencies ?? body.frequencies_xid; + + const activityTypeXid = rawActivityType !== undefined && rawActivityType !== null ? Number(rawActivityType) : undefined; + const frequenciesXid = rawFrequencies !== undefined && rawFrequencies !== null ? Number(rawFrequencies) : undefined; + + if (!activityTypeXid || isNaN(activityTypeXid)) { + throw new ApiError(400, 'activityTypeXid is required and must be a number'); + } + + // Create full activity and related records + const createdData = await hostService.createFullActivity(userInfo.id, body); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Activity created successfully', + data: createdData + }), + }; + }, +); diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index cf4449e..dc3c519 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -1867,6 +1867,278 @@ export class HostService { }); } + /** + * Create a full activity with related records based on payload from the onboarding form. + * This method will create Activities + ActivityOtherDetails + ActivitiesMedia + + * ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine + + * ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments + + * ActivityAmenities + ActivityEligibility and also seed PQQ headers. + */ + async createFullActivity(userId: number, payload: any) { + return await this.prisma.$transaction(async (tx) => { + const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true } }); + if (!host) throw new ApiError(404, 'Host not found for the user'); + + const activityTypeXid = payload.activityTypeXid ?? payload.activityType ?? payload.activity_type_xid; + if (!activityTypeXid) throw new ApiError(400, 'activityTypeXid is required'); + + const activityType = await tx.activityTypes.findUnique({ where: { id: Number(activityTypeXid) } }); + if (!activityType) throw new ApiError(404, 'Activity type not found'); + + const frequenciesXid = payload.frequenciesXid ?? payload.frequencies ?? payload.frequencies_xid; + if (frequenciesXid) { + const freq = await tx.frequencies.findUnique({ where: { id: Number(frequenciesXid) } }); + if (!freq) throw new ApiError(404, 'Frequency not found'); + } + + const referenceNumber = await generateActivityRefNumber(tx); + + const activityData: any = { + hostXid: host.id, + activityTypeXid: Number(activityTypeXid), + frequenciesXid: frequenciesXid ? Number(frequenciesXid) : null, + activityRefNumber: referenceNumber, + }; + + // Simple mappings + if (payload.activityTitle) activityData.activityTitle = String(payload.activityTitle).substring(0, 30); + if (payload.activityDescription) activityData.activityDescription = String(payload.activityDescription).substring(0, 80); + if (payload.checkInLat) activityData.checkInLat = Number(payload.checkInLat); + if (payload.checkInLong) activityData.checkInLong = Number(payload.checkInLong); + if (payload.checkInAddress) activityData.checkInAddress = String(payload.checkInAddress).substring(0,150); + if (payload.isCheckOutSame !== undefined) activityData.isCheckOutSame = Boolean(payload.isCheckOutSame); + if (payload.checkOutLat) activityData.checkOutLat = Number(payload.checkOutLat); + if (payload.checkOutLong) activityData.checkOutLong = Number(payload.checkOutLong); + if (payload.checkOutAddress) activityData.checkOutAddress = String(payload.checkOutAddress).substring(0,150); + if (payload.energyLevelXid) activityData.energyLevelXid = Number(payload.energyLevelXid); + + // duration: accept minutes or hours+minutes + if (payload.activityDurationMins) activityData.activityDurationMins = Number(payload.activityDurationMins); + else if (payload.durationHours || payload.durationMins) { + const hrs = Number(payload.durationHours || 0); + const mins = Number(payload.durationMins || 0); + activityData.activityDurationMins = hrs * 60 + mins; + } + + // Booleans + const boolFields = [ + 'foodAvailable','foodIsChargeable','alcoholAvailable','trainerAvailable','trainerIsChargeable', + 'pickUpDropAvailable','pickUpDropIsChargeable','inActivityAvailable','inActivityIsChargeable', + 'equipmentAvailable','equipmentIsChargeable','cancellationAvailable','isInstantBooking' + ]; + for (const k of boolFields) { + if (payload[k] !== undefined) { + activityData[k] = Boolean(payload[k]); + } + } + + if (payload.currencyXid) activityData.currencyXid = Number(payload.currencyXid); + if (payload.sustainabilityScore) activityData.sustainabilityScore = Number(payload.sustainabilityScore); + if (payload.safetyScore) activityData.safetyScore = Number(payload.safetyScore); + + // Create activity + const createdActivity = await tx.activities.create({ data: activityData }); + + // Other details + if (payload.exclusiveNotes || payload.dosNotes || payload.dontsNotes || payload.tipsNotes || payload.termsAndCondition) { + await tx.activityOtherDetails.create({ + data: { + activityXid: createdActivity.id, + exclusiveNotes: payload.exclusiveNotes ? String(payload.exclusiveNotes).substring(0,50) : null, + dosNotes: payload.dosNotes ? String(payload.dosNotes).substring(0,200) : null, + dontsNotes: payload.dontsNotes ? String(payload.dontsNotes).substring(0,200) : null, + tipsNotes: payload.tipsNotes ? String(payload.tipsNotes).substring(0,100) : null, + termsAndCondition: payload.termsAndCondition ? String(payload.termsAndCondition).substring(0,500) : null, + } + }); + } + + // Media + if (Array.isArray(payload.media) && payload.media.length) { + const mediaData = payload.media.map((m: any, idx: number) => ({ + activityXid: createdActivity.id, + mediaType: String(m.mediaType || 'image'), + mediaFileName: String(m.mediaFileName), + displayOrder: idx + 1, + })); + await tx.activitiesMedia.createMany({ data: mediaData }); + } + + // Venues + Prices + if (Array.isArray(payload.venues) && payload.venues.length) { + for (const venue of payload.venues) { + const v = await tx.activityVenues.create({ + data: { + activityXid: createdActivity.id, + venueName: String(venue.venueName).substring(0,50), + venueCapacity: venue.venueCapacity ? Number(venue.venueCapacity) : 0, + availableSeats: venue.availableSeats ? Number(venue.availableSeats) : 0, + isMinPeopleReqMandatory: Boolean(venue.isMinPeopleReqMandatory || false), + minPeopleRequired: venue.minPeopleRequired ? Number(venue.minPeopleRequired) : null, + minReqfullfilledBeforeMins: venue.minReqfullfilledBeforeMins ? Number(venue.minReqfullfilledBeforeMins) : null, + venueDescription: venue.venueDescription ? String(venue.venueDescription).substring(0,200) : null, + } + }); + + if (Array.isArray(venue.prices) && venue.prices.length) { + const pricesData = venue.prices.map((p: any) => ({ + activityVenueXid: v.id, + noOfSession: Number(p.noOfSession || 1), + isPackage: Boolean(p.isPackage || false), + sessionValidity: Number(p.sessionValidity || 0), + sessionValidityFrequency: String(p.sessionValidityFrequency || 'Days'), + basePrice: Number(p.basePrice || 0), + sellPrice: Number(p.sellPrice || 0), + })); + await tx.activityPrices.createMany({ data: pricesData }); + } + } + } + + // Food types + if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) { + const ft = payload.foodTypeIds.map((id: any) => ({ activityXid: createdActivity.id, foodTypeXid: Number(id) })); + await tx.activityFoodTypes.createMany({ data: ft }); + } + + // Cuisines + if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) { + const cs = payload.cuisineIds.map((id: any) => ({ activityXid: createdActivity.id, foodCuisineXid: Number(id) })); + await tx.activityCuisine.createMany({ data: cs }); + } + + // Pick up transport + details + if (Array.isArray(payload.pickupTransports) && payload.pickupTransports.length) { + for (const pt of payload.pickupTransports) { + const transport = await tx.activityPickUpTransport.create({ + data: { + activityXid: createdActivity.id, + transportModeXid: Number(pt.transportModeXid), + isTransportModeChargeable: Boolean(pt.isTransportModeChargeable || false), + } + }); + + if (Array.isArray(pt.pickupDetails) && pt.pickupDetails.length) { + const pd = pt.pickupDetails.map((d: any) => ({ + activityPickUpTransportXid: transport.id, + isPickUp: Boolean(d.isPickUp || false), + locationLat: d.locationLat ? Number(d.locationLat) : null, + locationLong: d.locationLong ? Number(d.locationLong) : null, + locationAddress: d.locationAddress ? String(d.locationAddress).substring(0,150) : null, + transportBasePrice: d.transportBasePrice ? Number(d.transportBasePrice) : 0, + transportTotalPrice: d.transportTotalPrice ? Number(d.transportTotalPrice) : 0, + })); + await tx.activityPickUpDetails.createMany({ data: pd }); + } + } + } + + // Navigation modes + if (Array.isArray(payload.navigationModes) && payload.navigationModes.length) { + const navs = payload.navigationModes.map((n: any) => ({ + activityXid: createdActivity.id, + navigationModeXid: Number(n.navigationModeXid), + isInActivityChargeable: Boolean(n.isInActivityChargeable || false), + navigationModesBasePrice: Number(n.basePrice || 0), + navigationModesTotalPrice: Number(n.totalPrice || 0), + })); + await tx.activityNavigationModes.createMany({ data: navs }); + } + + // Equipments + if (Array.isArray(payload.equipments) && payload.equipments.length) { + const eqs = payload.equipments.map((e: any) => ({ + activityXid: createdActivity.id, + equipmentName: String(e.equipmentName).substring(0,30), + isEquipmentChargeable: Boolean(e.isEquipmentChargeable || false), + equipmentBasePrice: Number(e.equipmentBasePrice || 0), + equipmentTotalPrice: Number(e.equipmentTotalPrice || 0), + })); + await tx.activityEquipments.createMany({ data: eqs }); + } + + // Amenities + if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) { + const ams = payload.amenitiesIds.map((id: any) => ({ activityXid: createdActivity.id, amenitiesXid: Number(id) })); + await tx.activityAmenities.createMany({ data: ams }); + } + + // Eligibility + if (payload.eligibility) { + const e = payload.eligibility; + await tx.activityEligibility.create({ + data: { + activityXid: createdActivity.id, + isAgeRestriction: Boolean(e.isAgeRestriction || false), + ageRestrictionXid: e.ageRestrictionXid ? Number(e.ageRestrictionXid) : null, + isWeightRestriction: Boolean(e.isWeightRestriction || false), + weightRestrictionName: e.weightRestrictionName ? String(e.weightRestrictionName).substring(0,30) : null, + weightEntered: e.weightEntered ? Number(e.weightEntered) : null, + weightIn: e.weightIn ? String(e.weightIn).substring(0,30) : null, + minWeight: e.minWeight ? Number(e.minWeight) : null, + maxWeight: e.maxWeight ? Number(e.maxWeight) : null, + isHeightRestriction: Boolean(e.isHeightRestriction || false), + heightRestrictionName: e.heightRestrictionName ? String(e.heightRestrictionName).substring(0,30) : null, + heightEntered: e.heightEntered ? Number(e.heightEntered) : null, + minHeight: e.minHeight ? Number(e.minHeight) : null, + maxHeight: e.maxHeight ? Number(e.maxHeight) : null, + } + }); + } + + // ----------------- PQQ seeding (copy of existing logic) ----------------- + const questions = await tx.pQQCategories.findMany({ + where: { isActive: true }, + select: { + id: true, + categoryName: true, + displayOrder: true, + pqqsubCategories: { + where: { isActive: true }, + select: { + id: true, + subCategoryName: true, + displayOrder: true, + questions: { + where: { isActive: true }, + select: { + id: true, + questionName: true, + maxPoints: true, + displayOrder: true, + }, + orderBy: { displayOrder: 'asc' }, + }, + }, + orderBy: { displayOrder: 'asc' }, + }, + }, + orderBy: { displayOrder: 'asc' }, + }); + + const allQuestions: number[] = []; + for (const cat of questions) { + for (const sub of cat.pqqsubCategories) { + for (const q of sub.questions) { + allQuestions.push(q.id); + } + } + } + + if (allQuestions.length) { + await tx.activityPQQheader.createMany({ + data: allQuestions.map((id) => ({ + activityXid: createdActivity.id, + pqqQuestionXid: id, + pqqAnswerXid: null, + })), + }); + } + + return { activity_xid: createdActivity.id }; + }); + } + async getAllPQUpdatedResponse(activityXid: number) { const pqqHeaderData = await this.prisma.activityPQQheader.findMany({ From 2e4f31868467a36b9ffa54529ad27f5ccee186e1 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 18 Dec 2025 20:05:30 +0530 Subject: [PATCH 21/24] Add CreateActivityDto and update createFullActivity method for activity creation --- src/modules/host/dto/createActivity.schema.ts | 125 ++++++++++++++++++ .../OnBoarding/CreateNewActivity.ts | 22 ++- src/modules/host/services/host.service.ts | 58 ++++---- 3 files changed, 166 insertions(+), 39 deletions(-) create mode 100644 src/modules/host/dto/createActivity.schema.ts diff --git a/src/modules/host/dto/createActivity.schema.ts b/src/modules/host/dto/createActivity.schema.ts new file mode 100644 index 0000000..8cbd928 --- /dev/null +++ b/src/modules/host/dto/createActivity.schema.ts @@ -0,0 +1,125 @@ +import { z } from 'zod'; + +export const MediaDto = z.object({ + mediaType: z.string().optional(), + mediaFileName: z.string(), +}); + +export const PriceDto = z.object({ + noOfSession: z.number().int().optional().default(1), + isPackage: z.boolean().optional().default(false), + sessionValidity: z.number().int().optional().default(0), + sessionValidityFrequency: z.string().optional().default('Days'), + basePrice: z.number().int().optional().default(0), + sellPrice: z.number().int().optional().default(0), +}); + +export const VenueDto = z.object({ + venueName: z.string(), + venueCapacity: z.number().int().optional().default(0), + availableSeats: z.number().int().optional().default(0), + isMinPeopleReqMandatory: z.boolean().optional().default(false), + minPeopleRequired: z.number().int().nullable().optional(), + minReqfullfilledBeforeMins: z.number().int().nullable().optional(), + venueDescription: z.string().optional(), + prices: z.array(PriceDto).optional().default([]), +}); + +export const PickupDetailDto = z.object({ + isPickUp: z.boolean().optional().default(false), + locationLat: z.number().optional().nullable(), + locationLong: z.number().optional().nullable(), + locationAddress: z.string().optional().nullable(), + transportBasePrice: z.number().int().optional().default(0), + transportTotalPrice: z.number().int().optional().default(0), +}); + +export const PickupTransportDto = z.object({ + transportModeXid: z.number().int(), + isTransportModeChargeable: z.boolean().optional().default(false), + pickupDetails: z.array(PickupDetailDto).optional().default([]), +}); + +export const NavigationModeDto = z.object({ + navigationModeXid: z.number().int(), + isInActivityChargeable: z.boolean().optional().default(false), + basePrice: z.number().int().optional().default(0), + totalPrice: z.number().int().optional().default(0), +}); + +export const EquipmentDto = z.object({ + equipmentName: z.string(), + isEquipmentChargeable: z.boolean().optional().default(false), + equipmentBasePrice: z.number().int().optional().default(0), + equipmentTotalPrice: z.number().int().optional().default(0), +}); + +export const EligibilityDto = z.object({ + isAgeRestriction: z.boolean().optional().default(false), + ageRestrictionXid: z.number().int().nullable().optional(), + isWeightRestriction: z.boolean().optional().default(false), + weightRestrictionName: z.string().optional().nullable(), + weightEntered: z.number().int().nullable().optional(), + weightIn: z.string().optional().nullable(), + minWeight: z.number().int().nullable().optional(), + maxWeight: z.number().int().nullable().optional(), + isHeightRestriction: z.boolean().optional().default(false), + heightRestrictionName: z.string().optional().nullable(), + heightEntered: z.number().int().nullable().optional(), + minHeight: z.number().int().nullable().optional(), + maxHeight: z.number().int().nullable().optional(), +}); + +export const OtherDetailsDto = z.object({ + exclusiveNotes: z.string().optional(), + dosNotes: z.string().optional(), + dontsNotes: z.string().optional(), + tipsNotes: z.string().optional(), + termsAndCondition: z.string().optional(), +}); + +export const CreateActivityDto = z.object({ + activityTypeXid: z.number().int(), + frequenciesXid: z.number().int().nullable().optional(), + activityTitle: z.string().optional(), + activityDescription: z.string().optional(), + checkInLat: z.number().optional().nullable(), + checkInLong: z.number().optional().nullable(), + checkInAddress: z.string().optional().nullable(), + isCheckOutSame: z.boolean().optional().default(true), + checkOutLat: z.number().optional().nullable(), + checkOutLong: z.number().optional().nullable(), + checkOutAddress: z.string().optional().nullable(), + energyLevelXid: z.number().int().nullable().optional(), + activityDurationMins: z.number().int().nullable().optional(), + durationHours: z.number().int().optional(), + durationMins: z.number().int().optional(), + foodAvailable: z.boolean().optional().default(false), + foodIsChargeable: z.boolean().optional().default(false), + alcoholAvailable: z.boolean().optional().default(false), + trainerAvailable: z.boolean().optional().default(false), + trainerIsChargeable: z.boolean().optional().default(false), + pickUpDropAvailable: z.boolean().optional().default(false), + pickUpDropIsChargeable: z.boolean().optional().default(false), + inActivityAvailable: z.boolean().optional().default(false), + inActivityIsChargeable: z.boolean().optional().default(false), + equipmentAvailable: z.boolean().optional().default(false), + equipmentIsChargeable: z.boolean().optional().default(false), + cancellationAvailable: z.boolean().optional().default(false), + currencyXid: z.number().int().nullable().optional(), + sustainabilityScore: z.number().int().optional().nullable(), + safetyScore: z.number().int().optional().nullable(), + isInstantBooking: z.boolean().optional().default(false), + media: z.array(MediaDto).optional().default([]), + venues: z.array(VenueDto).optional().default([]), + foodTypeIds: z.array(z.number().int()).optional().default([]), + cuisineIds: z.array(z.number().int()).optional().default([]), + pickupTransports: z.array(PickupTransportDto).optional().default([]), + navigationModes: z.array(NavigationModeDto).optional().default([]), + equipments: z.array(EquipmentDto).optional().default([]), + amenitiesIds: z.array(z.number().int()).optional().default([]), + eligibility: EligibilityDto.optional(), + otherDetails: OtherDetailsDto.optional(), +}); + +export type CreateActivityInput = z.infer; diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts index 59e15a5..e0aaa60 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -5,6 +5,7 @@ import { Context, } from 'aws-lambda'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { CreateActivityDto } from '../../../dto/createActivity.schema'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import ApiError from '../../../../../common/utils/helper/ApiError'; import { HostService } from '../../../services/host.service'; @@ -29,26 +30,23 @@ export const handler = safeHandler( // Verify token and get user info const userInfo = await verifyHostToken(token); - let body: any = {}; + let rawBody: any = {}; try { - body = event.body ? JSON.parse(event.body) : {}; + rawBody = event.body ? JSON.parse(event.body) : {}; } catch (err) { throw new ApiError(400, 'Invalid JSON in request body'); } - // Accept multiple possible keys from the frontend payload - const rawActivityType = body.activityTypeXid ?? body.activityType ?? body.activity_type_xid; - const rawFrequencies = body.frequenciesXid ?? body.frequencies ?? body.frequencies_xid; - - const activityTypeXid = rawActivityType !== undefined && rawActivityType !== null ? Number(rawActivityType) : undefined; - const frequenciesXid = rawFrequencies !== undefined && rawFrequencies !== null ? Number(rawFrequencies) : undefined; - - if (!activityTypeXid || isNaN(activityTypeXid)) { - throw new ApiError(400, 'activityTypeXid is required and must be a number'); + // Validate request body with Zod DTO + let dto: any; + try { + dto = CreateActivityDto.parse(rawBody); + } catch (err: any) { + throw new ApiError(400, 'Invalid payload: ' + (err?.message || 'validation failed')); } // Create full activity and related records - const createdData = await hostService.createFullActivity(userInfo.id, body); + const createdData = await hostService.createFullActivity(userInfo.id, dto as any); return { statusCode: 200, diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index dc3c519..5ceb87b 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -1874,18 +1874,18 @@ export class HostService { * ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments + * ActivityAmenities + ActivityEligibility and also seed PQQ headers. */ - async createFullActivity(userId: number, payload: any) { + async createFullActivity(userId: number, payload: import('../dto/createActivity.schema').CreateActivityInput) { return await this.prisma.$transaction(async (tx) => { const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true } }); if (!host) throw new ApiError(404, 'Host not found for the user'); - const activityTypeXid = payload.activityTypeXid ?? payload.activityType ?? payload.activity_type_xid; + const activityTypeXid = payload.activityTypeXid; if (!activityTypeXid) throw new ApiError(400, 'activityTypeXid is required'); const activityType = await tx.activityTypes.findUnique({ where: { id: Number(activityTypeXid) } }); if (!activityType) throw new ApiError(404, 'Activity type not found'); - const frequenciesXid = payload.frequenciesXid ?? payload.frequencies ?? payload.frequencies_xid; + const frequenciesXid = payload.frequenciesXid ?? null; if (frequenciesXid) { const freq = await tx.frequencies.findUnique({ where: { id: Number(frequenciesXid) } }); if (!freq) throw new ApiError(404, 'Frequency not found'); @@ -1903,12 +1903,12 @@ export class HostService { // Simple mappings if (payload.activityTitle) activityData.activityTitle = String(payload.activityTitle).substring(0, 30); if (payload.activityDescription) activityData.activityDescription = String(payload.activityDescription).substring(0, 80); - if (payload.checkInLat) activityData.checkInLat = Number(payload.checkInLat); - if (payload.checkInLong) activityData.checkInLong = Number(payload.checkInLong); + if (payload.checkInLat !== undefined && payload.checkInLat !== null) activityData.checkInLat = Number(payload.checkInLat); + if (payload.checkInLong !== undefined && payload.checkInLong !== null) activityData.checkInLong = Number(payload.checkInLong); if (payload.checkInAddress) activityData.checkInAddress = String(payload.checkInAddress).substring(0,150); if (payload.isCheckOutSame !== undefined) activityData.isCheckOutSame = Boolean(payload.isCheckOutSame); - if (payload.checkOutLat) activityData.checkOutLat = Number(payload.checkOutLat); - if (payload.checkOutLong) activityData.checkOutLong = Number(payload.checkOutLong); + if (payload.checkOutLat !== undefined && payload.checkOutLat !== null) activityData.checkOutLat = Number(payload.checkOutLat); + if (payload.checkOutLong !== undefined && payload.checkOutLong !== null) activityData.checkOutLong = Number(payload.checkOutLong); if (payload.checkOutAddress) activityData.checkOutAddress = String(payload.checkOutAddress).substring(0,150); if (payload.energyLevelXid) activityData.energyLevelXid = Number(payload.energyLevelXid); @@ -1920,35 +1920,39 @@ export class HostService { activityData.activityDurationMins = hrs * 60 + mins; } - // Booleans - const boolFields = [ - 'foodAvailable','foodIsChargeable','alcoholAvailable','trainerAvailable','trainerIsChargeable', - 'pickUpDropAvailable','pickUpDropIsChargeable','inActivityAvailable','inActivityIsChargeable', - 'equipmentAvailable','equipmentIsChargeable','cancellationAvailable','isInstantBooking' - ]; - for (const k of boolFields) { - if (payload[k] !== undefined) { - activityData[k] = Boolean(payload[k]); - } - } + // Booleans (assign explicitly for type-safety) + if (payload.foodAvailable !== undefined) activityData.foodAvailable = payload.foodAvailable; + if (payload.foodIsChargeable !== undefined) activityData.foodIsChargeable = payload.foodIsChargeable; + if (payload.alcoholAvailable !== undefined) activityData.alcoholAvailable = payload.alcoholAvailable; + if (payload.trainerAvailable !== undefined) activityData.trainerAvailable = payload.trainerAvailable; + if (payload.trainerIsChargeable !== undefined) activityData.trainerIsChargeable = payload.trainerIsChargeable; + if (payload.pickUpDropAvailable !== undefined) activityData.pickUpDropAvailable = payload.pickUpDropAvailable; + if (payload.pickUpDropIsChargeable !== undefined) activityData.pickUpDropIsChargeable = payload.pickUpDropIsChargeable; + if (payload.inActivityAvailable !== undefined) activityData.inActivityAvailable = payload.inActivityAvailable; + if (payload.inActivityIsChargeable !== undefined) activityData.inActivityIsChargeable = payload.inActivityIsChargeable; + if (payload.equipmentAvailable !== undefined) activityData.equipmentAvailable = payload.equipmentAvailable; + if (payload.equipmentIsChargeable !== undefined) activityData.equipmentIsChargeable = payload.equipmentIsChargeable; + if (payload.cancellationAvailable !== undefined) activityData.cancellationAvailable = payload.cancellationAvailable; + if (payload.isInstantBooking !== undefined) activityData.isInstantBooking = payload.isInstantBooking; if (payload.currencyXid) activityData.currencyXid = Number(payload.currencyXid); - if (payload.sustainabilityScore) activityData.sustainabilityScore = Number(payload.sustainabilityScore); - if (payload.safetyScore) activityData.safetyScore = Number(payload.safetyScore); + if (payload.sustainabilityScore !== undefined && payload.sustainabilityScore !== null) activityData.sustainabilityScore = Number(payload.sustainabilityScore); + if (payload.safetyScore !== undefined && payload.safetyScore !== null) activityData.safetyScore = Number(payload.safetyScore); // Create activity const createdActivity = await tx.activities.create({ data: activityData }); - // Other details - if (payload.exclusiveNotes || payload.dosNotes || payload.dontsNotes || payload.tipsNotes || payload.termsAndCondition) { + // Other details (nested DTO) + const od = (payload as any).otherDetails; + if (od && (od.exclusiveNotes || od.dosNotes || od.dontsNotes || od.tipsNotes || od.termsAndCondition)) { await tx.activityOtherDetails.create({ data: { activityXid: createdActivity.id, - exclusiveNotes: payload.exclusiveNotes ? String(payload.exclusiveNotes).substring(0,50) : null, - dosNotes: payload.dosNotes ? String(payload.dosNotes).substring(0,200) : null, - dontsNotes: payload.dontsNotes ? String(payload.dontsNotes).substring(0,200) : null, - tipsNotes: payload.tipsNotes ? String(payload.tipsNotes).substring(0,100) : null, - termsAndCondition: payload.termsAndCondition ? String(payload.termsAndCondition).substring(0,500) : null, + exclusiveNotes: od.exclusiveNotes ? String(od.exclusiveNotes).substring(0,50) : null, + dosNotes: od.dosNotes ? String(od.dosNotes).substring(0,200) : null, + dontsNotes: od.dontsNotes ? String(od.dontsNotes).substring(0,200) : null, + tipsNotes: od.tipsNotes ? String(od.tipsNotes).substring(0,100) : null, + termsAndCondition: od.termsAndCondition ? String(od.termsAndCondition).substring(0,500) : null, } }); } From 5a223f126f346fdac9e9a6b3da48e624a25cee6e Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Sun, 21 Dec 2025 17:28:08 +0530 Subject: [PATCH 22/24] creat activity handler --- prisma/schema.prisma | 8 +- serverless.yml | 3 +- serverless/functions/host.yml | 17 + src/common/utils/constants/host.constant.ts | 147 +++-- .../utils/constants/minglar.constant.ts | 5 +- src/modules/host/dto/createActivity.schema.ts | 81 ++- .../OnBoarding/CreateNewActivity.ts | 292 +++++++-- src/modules/host/services/host.service.ts | 598 +++++++++++------- 8 files changed, 769 insertions(+), 382 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e902b9c..ca8a446 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -913,7 +913,6 @@ model Activities { ActivityEligibility ActivityEligibility[] ActivitySuggestions ActivitySuggestions[] ActivityAmDetails ActivityAmDetails[] - ActivityPrices ActivityPrices[] ActivityVenueArtifacts ActivityVenueArtifacts[] ActivityPQQheader ActivityPQQheader[] ActivityAllowedEntry ActivityAllowedEntry[] @@ -922,7 +921,6 @@ model Activities { ActivityNavigationModes ActivityNavigationModes[] ActivityPickUpDetails ActivityPickUpDetails[] ActivityAmenities ActivityAmenities[] - ActivityEquipmentTaxes ActivityEquipmentTaxes[] ScheduleHeader ScheduleHeader[] ItineraryActivities ItineraryActivities[] activityTracks ActivityTrack[] @@ -1004,6 +1002,7 @@ model ActivityVenues { deletedAt DateTime? @map("deleted_at") ScheduleHeader ScheduleHeader[] ItineraryActivities ItineraryActivities[] + ActivityPrices ActivityPrices[] // <-- Added opposite relation @@map("activity_venues") @@schema("act") @@ -1107,7 +1106,7 @@ model ActivityAmDetails { model ActivityPrices { id Int @id @default(autoincrement()) activityVenueXid Int @map("activity_venue_xid") - activityVenue Activities @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade) + activityVenue ActivityVenues @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade) noOfSession Int @map("no_of_session") isPackage Boolean @default(false) @map("is_package") sessionValidity Int @map("session_validity") @@ -1302,6 +1301,7 @@ model ActivityEquipments { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") + ActivityEquipmentTaxes ActivityEquipmentTaxes[] @@map("activity_equipments") @@schema("act") @@ -1310,7 +1310,7 @@ model ActivityEquipments { model ActivityEquipmentTaxes { id Int @id @default(autoincrement()) activityEquipmentXid Int @map("activity_equipment_xid") - activityEquipment Activities @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade) + activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade) taxXid Int @map("tax_xid") taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict) taxPer Float @map("tax_per") diff --git a/serverless.yml b/serverless.yml index a1ec7ae..c0d1f26 100644 --- a/serverless.yml +++ b/serverless.yml @@ -21,7 +21,8 @@ provider: # Reference the layer defined in this stack using CloudFormation Ref layers: # Use the exported stack output so deploy function works (expects a string ARN) - - ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn} + # For offline/local, fall back to an empty string so the CF lookup is optional. + - ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''} apiGateway: binaryMediaTypes: - '*/*' diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index 117b18c..2b1d8f9 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -177,6 +177,23 @@ prePopulateNewActivity: path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity method: get +createNewActivity: + handler: src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.handler + memorySize: 1024 + timeout: 30 + package: + patterns: + - 'src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.*' + - 'src/modules/host/services/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /host/Activity_Hub/OnBoarding/create-new-activity + method: patch + showSuggestion: handler: src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler memorySize: 384 diff --git a/src/common/utils/constants/host.constant.ts b/src/common/utils/constants/host.constant.ts index 4cccf6e..3dbe85f 100644 --- a/src/common/utils/constants/host.constant.ts +++ b/src/common/utils/constants/host.constant.ts @@ -1,73 +1,100 @@ export const HOST_STATUS_INTERNAL = { - HOST_SUBMITTED: "Host Submitted", - HOST_TO_UPDATE: "Host To Update", - REJECTED: "Rejected", - APPROVED: "Approved", - DRAFT: "Draft", -} + HOST_SUBMITTED: 'Host Submitted', + HOST_TO_UPDATE: 'Host To Update', + REJECTED: 'Rejected', + APPROVED: 'Approved', + DRAFT: 'Draft', +}; export const HOST_STATUS_DISPLAY = { - DRAFT: "Draft", - UNDER_REVIEW: "Under Review", - ENHANCING: "Enhancing", - REJECTED: "Rejected", - APPROVED: "Approved", -} + DRAFT: 'Draft', + UNDER_REVIEW: 'Under Review', + ENHANCING: 'Enhancing', + REJECTED: 'Rejected', + APPROVED: 'Approved', +}; export const STEPPER = { - NOT_SUBMITTED: 1, - UNDER_REVIEW: 2, - COMPANY_DETAILS_APPROVED: 3, - BANK_DETAILS_UPDATED: 4, - AGREEMENT_ACCEPTED: 5, - REJECTED: 6 -} + NOT_SUBMITTED: 1, + UNDER_REVIEW: 2, + COMPANY_DETAILS_APPROVED: 3, + BANK_DETAILS_UPDATED: 4, + AGREEMENT_ACCEPTED: 5, + REJECTED: 6, +}; export const ACTIVITY_INTERNAL_STATUS = { - DRAFT_PQ: 'Draft - PQ', - APPROVED: 'Approved', - REJECTED: 'Rejected', - DRAFT: 'Draft', - UNDER_REVIEW: 'Under-Review', - PQ_FAILED: 'PQ Failed', - PQ_TO_UPDATE: 'PQ To Update', - PQ_SUBMITTED: 'PQ Submitted', - PQ_APPROVED: 'PQ Approved' -} + DRAFT_PQ: 'Draft - PQ', + APPROVED: 'Approved', + REJECTED: 'Rejected', + DRAFT: 'Draft', + UNDER_REVIEW: 'Under-Review', + PQ_FAILED: 'PQ Failed', + PQ_TO_UPDATE: 'PQ To Update', + PQ_SUBMITTED: 'PQ Submitted', + PQ_APPROVED: 'PQ Approved', + + ACTIVITY_DRAFT: 'Draft - Activity', + ACTIVITY_SUBMITTED: 'Activity Submitted', + ACTIVITY_TO_REVIEW: 'Activity To Review', + ACTIVITY_REJECTED: 'Activity Rejected', + ACTIVITY_APPROVED: 'Activity Approved', + ACTIVITY_LISTED: 'Activity Listed', + ACTIVITY_UNLISTED: 'Activity Un Listed By Host', +}; export const ACTIVITY_DISPLAY_STATUS = { - DRAFT_PQ: 'Draft - PQ', - APPROVED: 'Approved', - REJECTED: 'Rejected', - DRAFT: 'Draft', - UNDER_REVIEW: 'Under-Review', - PQ_FAILED: 'PQ Failed', - ENHANCING: 'Enchancing', - PQ_IN_REVIEW: 'PQ In Review', - PQ_APPROVED: 'PQ Approved' -} + DRAFT_PQ: 'Draft - PQ', + APPROVED: 'Approved', + REJECTED: 'Rejected', + DRAFT: 'Draft', + UNDER_REVIEW: 'Under-Review', + PQ_FAILED: 'PQ Failed', + ENHANCING: 'Enchancing', + PQ_IN_REVIEW: 'PQ In Review', + PQ_APPROVED: 'PQ Approved', + + ACTIVITY_DRAFT: 'Draft - Activity', + ACTIVITY_IN_REVIEW: 'In Review', + ACTIVITY_TO_REVIEW: 'To Review', + ACTIVITY_NOT_LISTED: 'Not Listed', + ACTIVITY_LISTED: 'Listed', + ACTIVITY_UNLISTED: 'Un Listed', +}; export const ACTIVITY_AM_INTERNAL_STATUS = { - DRAFT_PQ: 'Draft - PQ', - APPROVED: 'Approved', - REJECTED: 'Rejected', - DRAFT: 'Draft', - UNDER_REVIEW: 'Under-Review', - PQ_FAILED: 'PQ Failed', - PQ_REJECTED: 'PQ Rejected', - PQ_TO_REVIEW: 'PQ To Review', - PQ_APPROVED: 'PQ Approved' -} + DRAFT_PQ: 'Draft - PQ', + APPROVED: 'Approved', + REJECTED: 'Rejected', + DRAFT: 'Draft', + UNDER_REVIEW: 'Under-Review', + PQ_FAILED: 'PQ Failed', + PQ_REJECTED: 'PQ Rejected', + PQ_TO_REVIEW: 'PQ To Review', + PQ_APPROVED: 'PQ Approved', + + ACTIVITY_DRAFT: 'Draft - Activity', + ACTIVITY_TO_REVIEW: 'Activity To Review', + ACTIVITY_REJECTED: 'Activity Rejected', + ACTIVITY_APPROVED: 'Activity Approved', + ACTIVITY_LISTED: 'Activity Listed', +}; export const ACTIVITY_AM_DISPLAY_STATUS = { - DRAFT_PQ: 'Draft - PQ', - APPROVED: 'Approved', - REJECTED: 'Rejected', - DRAFT: 'Draft', - UNDER_REVIEW: 'Under-Review', - PQ_FAILED: 'PQ Failed', - ENHANCING: 'Enchancing', - NEW: 'New', - PQ_APPROVED: 'PQ Approved', - REVISED: 'Revised' -} \ No newline at end of file + DRAFT_PQ: 'Draft - PQ', + APPROVED: 'Approved', + REJECTED: 'Rejected', + DRAFT: 'Draft', + UNDER_REVIEW: 'Under-Review', + PQ_FAILED: 'PQ Failed', + ENHANCING: 'Enchancing', + NEW: 'New', + PQ_APPROVED: 'PQ Approved', + REVISED: 'Revised', + + ACTIVITY_DRAFT: 'Draft - Activity', + ACTIVITY_NEW: 'To Review', + ACTIVITY_ENHANCING: 'Enhancing', + ACTIVITY_NOT_LISTED: 'Not Listed', + ACTIVITY_LISTED: 'Listed', +}; diff --git a/src/common/utils/constants/minglar.constant.ts b/src/common/utils/constants/minglar.constant.ts index 0287b62..8c8c639 100644 --- a/src/common/utils/constants/minglar.constant.ts +++ b/src/common/utils/constants/minglar.constant.ts @@ -34,7 +34,10 @@ export const ACTIVITY_TRACK_STATUS = { REJECTED_BY_AM: 'Rejected By AM', ACCEPTED_BY_AM: 'Accepted By AM', ENHANCING: 'Enhancing', - PQ_SUBMITTED: 'PQ Submitted' + PQ_SUBMITTED: 'PQ Submitted', + UNDER_REVIEW:'Under Review', + SUBMITTED:'Activity Submitted', + DRAFT:'Activity Draft' } // export const HOST_SUGGESTION_TITLES = { diff --git a/src/modules/host/dto/createActivity.schema.ts b/src/modules/host/dto/createActivity.schema.ts index 8cbd928..5032530 100644 --- a/src/modules/host/dto/createActivity.schema.ts +++ b/src/modules/host/dto/createActivity.schema.ts @@ -1,19 +1,24 @@ import { z } from 'zod'; +/* ================= MEDIA ================= */ export const MediaDto = z.object({ mediaType: z.string().optional(), mediaFileName: z.string(), }); +/* ================= PRICE ================= + * ❌ NO TAX HERE (tax is root-level only) + */ export const PriceDto = z.object({ noOfSession: z.number().int().optional().default(1), isPackage: z.boolean().optional().default(false), sessionValidity: z.number().int().optional().default(0), sessionValidityFrequency: z.string().optional().default('Days'), basePrice: z.number().int().optional().default(0), - sellPrice: z.number().int().optional().default(0), + sellPrice: z.number().int(), // REQUIRED }); +/* ================= VENUE ================= */ export const VenueDto = z.object({ venueName: z.string(), venueCapacity: z.number().int().optional().default(0), @@ -25,11 +30,12 @@ export const VenueDto = z.object({ prices: z.array(PriceDto).optional().default([]), }); +/* ================= PICKUP / DROP ================= */ export const PickupDetailDto = z.object({ isPickUp: z.boolean().optional().default(false), - locationLat: z.number().optional().nullable(), - locationLong: z.number().optional().nullable(), - locationAddress: z.string().optional().nullable(), + locationLat: z.number().nullable().optional(), + locationLong: z.number().nullable().optional(), + locationAddress: z.string().nullable().optional(), transportBasePrice: z.number().int().optional().default(0), transportTotalPrice: z.number().int().optional().default(0), }); @@ -40,13 +46,7 @@ export const PickupTransportDto = z.object({ pickupDetails: z.array(PickupDetailDto).optional().default([]), }); -export const NavigationModeDto = z.object({ - navigationModeXid: z.number().int(), - isInActivityChargeable: z.boolean().optional().default(false), - basePrice: z.number().int().optional().default(0), - totalPrice: z.number().int().optional().default(0), -}); - +/* ================= EQUIPMENT ================= */ export const EquipmentDto = z.object({ equipmentName: z.string(), isEquipmentChargeable: z.boolean().optional().default(false), @@ -54,22 +54,26 @@ export const EquipmentDto = z.object({ equipmentTotalPrice: z.number().int().optional().default(0), }); +/* ================= ELIGIBILITY ================= */ export const EligibilityDto = z.object({ isAgeRestriction: z.boolean().optional().default(false), ageRestrictionXid: z.number().int().nullable().optional(), + isWeightRestriction: z.boolean().optional().default(false), - weightRestrictionName: z.string().optional().nullable(), + weightRestrictionName: z.string().nullable().optional(), weightEntered: z.number().int().nullable().optional(), - weightIn: z.string().optional().nullable(), + weightIn: z.string().nullable().optional(), minWeight: z.number().int().nullable().optional(), maxWeight: z.number().int().nullable().optional(), + isHeightRestriction: z.boolean().optional().default(false), - heightRestrictionName: z.string().optional().nullable(), + heightRestrictionName: z.string().nullable().optional(), heightEntered: z.number().int().nullable().optional(), minHeight: z.number().int().nullable().optional(), maxHeight: z.number().int().nullable().optional(), }); +/* ================= OTHER DETAILS ================= */ export const OtherDetailsDto = z.object({ exclusiveNotes: z.string().optional(), dosNotes: z.string().optional(), @@ -78,46 +82,75 @@ export const OtherDetailsDto = z.object({ termsAndCondition: z.string().optional(), }); +/* ================= CREATE ACTIVITY ================= */ export const CreateActivityDto = z.object({ - activityTypeXid: z.number().int(), + /* 🔑 REQUIRED */ + activityXid: z.number().int(), + + /* OPTIONAL CORE */ + activityTypeXid: z.number().int().optional(), frequenciesXid: z.number().int().nullable().optional(), activityTitle: z.string().optional(), activityDescription: z.string().optional(), - checkInLat: z.number().optional().nullable(), - checkInLong: z.number().optional().nullable(), - checkInAddress: z.string().optional().nullable(), + + /* LOCATION */ + checkInLat: z.number().nullable().optional(), + checkInLong: z.number().nullable().optional(), + checkInAddress: z.string().nullable().optional(), isCheckOutSame: z.boolean().optional().default(true), - checkOutLat: z.number().optional().nullable(), - checkOutLong: z.number().optional().nullable(), - checkOutAddress: z.string().optional().nullable(), + checkOutLat: z.number().nullable().optional(), + checkOutLong: z.number().nullable().optional(), + checkOutAddress: z.string().nullable().optional(), + + /* DURATION / ENERGY */ energyLevelXid: z.number().int().nullable().optional(), activityDurationMins: z.number().int().nullable().optional(), durationHours: z.number().int().optional(), durationMins: z.number().int().optional(), + + /* FLAGS */ foodAvailable: z.boolean().optional().default(false), foodIsChargeable: z.boolean().optional().default(false), alcoholAvailable: z.boolean().optional().default(false), + trainerAvailable: z.boolean().optional().default(false), trainerIsChargeable: z.boolean().optional().default(false), + pickUpDropAvailable: z.boolean().optional().default(false), pickUpDropIsChargeable: z.boolean().optional().default(false), + inActivityAvailable: z.boolean().optional().default(false), inActivityIsChargeable: z.boolean().optional().default(false), + equipmentAvailable: z.boolean().optional().default(false), equipmentIsChargeable: z.boolean().optional().default(false), + cancellationAvailable: z.boolean().optional().default(false), + + /* MONEY */ currencyXid: z.number().int().nullable().optional(), - sustainabilityScore: z.number().int().optional().nullable(), - safetyScore: z.number().int().optional().nullable(), + sustainabilityScore: z.number().int().nullable().optional(), + safetyScore: z.number().int().nullable().optional(), isInstantBooking: z.boolean().optional().default(false), + + /* 🔥 ROOT-LEVEL TAX (SINGLE SOURCE OF TRUTH) */ + taxXids: z.array(z.number().int()).optional().default([]), + + /* ARRAYS */ media: z.array(MediaDto).optional().default([]), venues: z.array(VenueDto).optional().default([]), + foodTypeIds: z.array(z.number().int()).optional().default([]), cuisineIds: z.array(z.number().int()).optional().default([]), + pickupTransports: z.array(PickupTransportDto).optional().default([]), - navigationModes: z.array(NavigationModeDto).optional().default([]), + + /* 🔥 NAVIGATION = IDs ONLY */ + navigationModes: z.array(z.number().int()).optional().default([]), + equipments: z.array(EquipmentDto).optional().default([]), amenitiesIds: z.array(z.number().int()).optional().default([]), + eligibility: EligibilityDto.optional(), otherDetails: OtherDetailsDto.optional(), }); diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts index e0aaa60..e6edb29 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -1,64 +1,256 @@ -import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; -import { - APIGatewayProxyEvent, - APIGatewayProxyResult, - Context, -} from 'aws-lambda'; +import config from '../../../../../config/config'; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import AWS from 'aws-sdk'; +import Busboy from 'busboy'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; -import { CreateActivityDto } from '../../../dto/createActivity.schema'; +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import ApiError from '../../../../../common/utils/helper/ApiError'; +import { + CreateActivityDto, + CreateActivityInput, +} from '../../../dto/createActivity.schema'; import { HostService } from '../../../services/host.service'; const hostService = new HostService(prismaClient); +const s3 = new AWS.S3({ region: config.aws.region }); + +/* ------------------------------- Utilities ------------------------------- */ + +function getExtensionFromMime(mimeType: string) { + const map: Record = { + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + // ✅ Common video formats + 'video/mp4': 'mp4', + 'video/quicktime': 'mov', + 'video/x-msvideo': 'avi', + 'video/x-matroska': 'mkv', + }; + return map[mimeType] || 'bin'; +} + +function normalizeJsonField(fields: any, key: string) { + if (!fields[key]) return undefined; + if (typeof fields[key] === 'object') return fields[key]; + + try { + return JSON.parse(fields[key]); + } catch { + throw new ApiError(400, `Invalid JSON in field: ${key}`); + } +} + +/* -------------------------------- Handler -------------------------------- */ export const handler = safeHandler( - async ( - event: APIGatewayProxyEvent, - context?: Context, - ): Promise => { - // Verify authentication token - const token = - event.headers['x-auth-token'] || event.headers['X-Auth-Token']; - if (!token) { - throw new ApiError( - 401, - 'This is a protected route. Please provide a valid token.', - ); - } + async (event: APIGatewayProxyEvent): Promise => { + /* 1️⃣ AUTH */ + const token = + event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError( + 401, + 'This is a protected route. Please provide a valid token.', + ); + } - // Verify token and get user info - const userInfo = await verifyHostToken(token); + const userInfo = await verifyHostToken(token); - let rawBody: any = {}; - try { - rawBody = event.body ? JSON.parse(event.body) : {}; - } catch (err) { - throw new ApiError(400, 'Invalid JSON in request body'); - } + /* 2️⃣ CONTENT TYPE */ + const contentType = + event.headers['content-type'] || event.headers['Content-Type']; + if (!contentType?.includes('multipart/form-data')) { + throw new ApiError(400, 'Content-Type must be multipart/form-data'); + } - // Validate request body with Zod DTO - let dto: any; - try { - dto = CreateActivityDto.parse(rawBody); - } catch (err: any) { - throw new ApiError(400, 'Invalid payload: ' + (err?.message || 'validation failed')); - } + /* 3️⃣ BODY BUFFER */ + const bodyBuffer = event.isBase64Encoded + ? Buffer.from(event.body as string, 'base64') + : Buffer.from(event.body as string); - // Create full activity and related records - const createdData = await hostService.createFullActivity(userInfo.id, dto as any); + const fields: Record = {}; + const files: Array<{ + buffer: Buffer; + mimeType: string; + fileName: string; + fieldName: string; + }> = []; - return { - statusCode: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - body: JSON.stringify({ - success: true, - message: 'Activity created successfully', - data: createdData - }), - }; - }, + await new Promise((resolve, reject) => { + const bb = Busboy({ + headers: { + ...event.headers, + 'content-type': contentType, + }, + }); + + bb.on('field', (name, value) => { + fields[name] = value; + }); + + bb.on('file', (fieldName, file, info) => { + const { filename, mimeType } = info; + const chunks: Buffer[] = []; + let size = 0; + const MAX_SIZE = 5 * 1024 * 1024; + + file.on('data', (chunk) => { + size += chunk.length; + if (size > MAX_SIZE) { + file.destroy(new Error('File exceeds 5MB limit')); + return; + } + chunks.push(chunk); + }); + + file.on('end', () => { + if (chunks.length > 0) { + files.push({ + buffer: Buffer.concat(chunks), + mimeType: mimeType || 'application/octet-stream', + fileName: filename || 'unknown', + fieldName, + }); + } + }); + }); + + bb.on('finish', () => resolve()); + bb.on('error', (err) => reject(new ApiError(400, err.message))); + + bb.end(bodyBuffer); + }); + + /* 4️⃣ FLAGS */ + const isDraft = fields.isDraft === 'true' || fields.isDraft === true; + + /* 5️⃣ ACTIVITY PAYLOAD */ + const activityPayload: any = normalizeJsonField(fields, 'activity'); + if (!activityPayload) { + throw new ApiError(400, 'activity payload is required'); + } + + /* 6️⃣ NORMALIZE IDS */ + if (activityPayload.activityXid) { + activityPayload.activityXid = Number(activityPayload.activityXid); + } + + const numberKeys = [ + 'currencyXid', + 'energyLevelXid', + 'activityDurationMins', + 'trainerTotalAmount', + 'pickupDropTotalPrice', + 'navigationModeTotalPrice', + ]; + + for (const key of numberKeys) { + if (activityPayload[key] !== undefined && activityPayload[key] !== null) { + activityPayload[key] = Number(activityPayload[key]); + } + } + + /* 7️⃣ NORMALIZE BOOLEANS */ + const booleanKeys = [ + 'isInstantBooking', + 'foodAvailable', + 'foodIsChargeable', + 'alcoholAvailable', + 'trainerAvailable', + 'trainerIsChargeable', + 'pickUpDropAvailable', + 'pickUpDropIsChargeable', + 'inActivityAvailable', + 'inActivityIsChargeable', + 'equipmentAvailable', + 'equipmentIsChargeable', + 'cancellationAvailable', + 'isCheckOutSame', + ]; + + for (const key of booleanKeys) { + if (activityPayload[key] === 'true') activityPayload[key] = true; + if (activityPayload[key] === 'false') activityPayload[key] = false; + } + + /* 8️⃣ UPLOAD MEDIA */ + const uploadedMedia: Array<{ mediaType?: string; mediaFileName: string }> = + []; + + // ✅ Accept both images and videos under multipart fields `images` or `videos` + for (const file of files.filter( + (f) => f.fieldName === 'images' || f.fieldName === 'videos', + )) { + const ext = getExtensionFromMime(file.mimeType); + + const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Artifacts/${file.fileName}`; + + if (s3Key.length > 900) { + throw new ApiError(400, 'Generated S3 key too long'); + } + + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: s3Key, + Body: file.buffer, + ContentType: file.mimeType, + ACL: 'private', + }) + .promise(); + + uploadedMedia.push({ + mediaType: file.mimeType, + mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, + }); + } + + /* 🔥 MERGE MEDIA (DO NOT OVERWRITE) */ + const existingMedia = Array.isArray(activityPayload.media) + ? activityPayload.media + : []; + + activityPayload.media = [...existingMedia, ...uploadedMedia]; + + /* 9️⃣ VALIDATION */ + let parsedDto: CreateActivityInput; + + if (!isDraft) { + const parsed = CreateActivityDto.safeParse(activityPayload); + if (!parsed.success) { + throw new ApiError( + 400, + parsed.error.issues.map((i) => i.message).join(', '), + ); + } + parsedDto = parsed.data; + } else { + parsedDto = activityPayload as CreateActivityInput; + } + + /* 🔟 SAVE ACTIVITY */ + const createdActivity = await hostService.createOrUpdateActivity( + userInfo.id, + parsedDto, + isDraft, + ); + + /* 1️⃣1️⃣ RESPONSE */ + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: isDraft + ? 'Activity saved as draft successfully' + : 'Activity created successfully', + data: createdActivity, + }), + }; + }, ); diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 5ceb87b..acbd213 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -68,6 +68,30 @@ function round2(value: number) { return Math.round(value); } +function computeBasePriceAndTaxes( + sellPrice: number, + taxes: Array<{ id: number; taxPer: number }>, +) { + if (!taxes?.length) { + return { + basePrice: round2(sellPrice), + taxDetails: [] as Array<{ taxXid: number; taxPer: number; taxAmount: number }>, + }; + } + + const totalTaxPer = taxes.reduce((sum, t) => sum + (Number(t.taxPer) || 0), 0); + const denominator = 1 + totalTaxPer / 100; + const basePrice = denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice); + + const taxDetails = taxes.map((t) => ({ + taxXid: t.id, + taxPer: t.taxPer, + taxAmount: round2(basePrice * (t.taxPer / 100)), + })); + + return { basePrice, taxDetails }; +} + const bucket = config.aws.bucketName; @Injectable() @@ -1872,276 +1896,366 @@ export class HostService { * This method will create Activities + ActivityOtherDetails + ActivitiesMedia + * ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine + * ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments + - * ActivityAmenities + ActivityEligibility and also seed PQQ headers. + * ActivityAmenities + ActivityEligibility */ - async createFullActivity(userId: number, payload: import('../dto/createActivity.schema').CreateActivityInput) { - return await this.prisma.$transaction(async (tx) => { - const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true } }); - if (!host) throw new ApiError(404, 'Host not found for the user'); +async createOrUpdateActivity( + userId: number, + payload: any, + isDraft: boolean, +) { + /* ===================================================== + * HELPERS + * ===================================================== */ + const toBool = (v: any) => + v === true || v === 'true' || v === 1 || v === '1'; - const activityTypeXid = payload.activityTypeXid; - if (!activityTypeXid) throw new ApiError(400, 'activityTypeXid is required'); + const toNumber = (v: any) => + v === undefined || v === null || v === '' ? undefined : Number(v); - const activityType = await tx.activityTypes.findUnique({ where: { id: Number(activityTypeXid) } }); - if (!activityType) throw new ApiError(404, 'Activity type not found'); + const round2 = (v: number) => Math.round(v); - const frequenciesXid = payload.frequenciesXid ?? null; - if (frequenciesXid) { - const freq = await tx.frequencies.findUnique({ where: { id: Number(frequenciesXid) } }); - if (!freq) throw new ApiError(404, 'Frequency not found'); - } + const computeBasePriceAndTaxes = ( + sellPrice: number, + taxes: Array<{ id: number; taxPer: number }>, + ) => { + if (!taxes.length) { + return { basePrice: round2(sellPrice), taxDetails: [] }; + } - const referenceNumber = await generateActivityRefNumber(tx); + const totalTaxPer = taxes.reduce((s, t) => s + Number(t.taxPer || 0), 0); + const basePrice = round2(sellPrice / (1 + totalTaxPer / 100)); - const activityData: any = { + return { + basePrice, + taxDetails: taxes.map((t) => ({ + taxXid: t.id, + taxPer: t.taxPer, + taxAmount: round2(basePrice * (t.taxPer / 100)), + })), + }; + }; + + /* ===================================================== + * BASIC GUARDS + * ===================================================== */ + if (!payload.activityXid) { + throw new ApiError(400, 'activityXid is required'); + } + + /* ===================================================== + * HARD NORMALIZATION (SERVICE-LEVEL) + * ===================================================== */ + payload.foodAvailable = toBool(payload.foodAvailable); + payload.alcoholAvailable = toBool(payload.alcoholAvailable); + payload.trainerAvailable = toBool(payload.trainerAvailable); + payload.pickUpDropAvailable = toBool(payload.pickUpDropAvailable); + payload.inActivityAvailable = toBool(payload.inActivityAvailable); + payload.equipmentAvailable = toBool(payload.equipmentAvailable); + payload.cancellationAvailable = toBool(payload.cancellationAvailable); + payload.isInstantBooking = toBool(payload.isInstantBooking); + payload.isCheckOutSame = toBool(payload.isCheckOutSame); + + payload.trainerTotalAmount = toNumber(payload.trainerTotalAmount); + + if (payload.trainerAvailable) { + if ( + typeof payload.trainerTotalAmount !== 'number' || + Number.isNaN(payload.trainerTotalAmount) || + payload.trainerTotalAmount <= 0 + ) { + throw new ApiError(400, 'trainerTotalAmount must be > 0'); + } + } else { + delete payload.trainerTotalAmount; + } + + if (payload.venues && !Array.isArray(payload.venues)) { + throw new ApiError(400, 'venues must be an array'); + } + + payload.venues?.forEach((v, idx) => { + v.isMinPeopleReqMandatory = toBool(v.isMinPeopleReqMandatory); + + if (!v.venueName) { + throw new ApiError(400, `venues[${idx}] venueName required`); + } + + if (v.isMinPeopleReqMandatory && !v.minPeopleRequired) { + throw new ApiError( + 400, + `venues[${idx}] min people requirement missing`, + ); + } + + if (!Array.isArray(v.prices) || !v.prices.length) { + throw new ApiError(400, `venues[${idx}] must have at least one price`); + } + }); + + /* ===================================================== + * ROOT TAX + * ===================================================== */ + const taxIds = Array.isArray(payload.taxXids) + ? payload.taxXids.map(Number) + : []; + + const rootTaxes = + taxIds.length > 0 + ? await this.prisma.taxes.findMany({ + where: { id: { in: taxIds }, isActive: true }, + select: { id: true, taxPer: true }, + }) + : []; + + if (taxIds.length !== rootTaxes.length) { + throw new ApiError(400, 'Invalid or inactive tax provided'); + } + + /* ===================================================== + * TRANSACTION + * ===================================================== */ + return await this.prisma.$transaction(async (tx) => { + /* -------------------------------- + * 1️⃣ HOST + * -------------------------------- */ + const host = await tx.hostHeader.findFirst({ + where: { userXid: userId, isActive: true }, + }); + if (!host) throw new ApiError(404, 'Host not found'); + + /* -------------------------------- + * 2️⃣ ACTIVITY + * -------------------------------- */ + const existingActivity = await tx.activities.findFirst({ + where: { + id: Number(payload.activityXid), hostXid: host.id, - activityTypeXid: Number(activityTypeXid), - frequenciesXid: frequenciesXid ? Number(frequenciesXid) : null, - activityRefNumber: referenceNumber, - }; + isActive: true, + }, + }); + if (!existingActivity) { + throw new ApiError(404, 'Activity not found'); + } - // Simple mappings - if (payload.activityTitle) activityData.activityTitle = String(payload.activityTitle).substring(0, 30); - if (payload.activityDescription) activityData.activityDescription = String(payload.activityDescription).substring(0, 80); - if (payload.checkInLat !== undefined && payload.checkInLat !== null) activityData.checkInLat = Number(payload.checkInLat); - if (payload.checkInLong !== undefined && payload.checkInLong !== null) activityData.checkInLong = Number(payload.checkInLong); - if (payload.checkInAddress) activityData.checkInAddress = String(payload.checkInAddress).substring(0,150); - if (payload.isCheckOutSame !== undefined) activityData.isCheckOutSame = Boolean(payload.isCheckOutSame); - if (payload.checkOutLat !== undefined && payload.checkOutLat !== null) activityData.checkOutLat = Number(payload.checkOutLat); - if (payload.checkOutLong !== undefined && payload.checkOutLong !== null) activityData.checkOutLong = Number(payload.checkOutLong); - if (payload.checkOutAddress) activityData.checkOutAddress = String(payload.checkOutAddress).substring(0,150); - if (payload.energyLevelXid) activityData.energyLevelXid = Number(payload.energyLevelXid); + /* -------------------------------- + * 3️⃣ STATUS DECISION (YOUR LOGIC) + * -------------------------------- */ + let activityInternalStatus; + let activityDisplayStatus; + let amInternalStatus; + let amDisplayStatus; - // duration: accept minutes or hours+minutes - if (payload.activityDurationMins) activityData.activityDurationMins = Number(payload.activityDurationMins); - else if (payload.durationHours || payload.durationMins) { - const hrs = Number(payload.durationHours || 0); - const mins = Number(payload.durationMins || 0); - activityData.activityDurationMins = hrs * 60 + mins; + const wasRejected = + existingActivity.activityInternalStatus === + ACTIVITY_INTERNAL_STATUS.ACTIVITY_REJECTED; + + if (wasRejected) { + if (isDraft) { + activityInternalStatus = + existingActivity.activityInternalStatus; + activityDisplayStatus = + existingActivity.activityDisplayStatus; + amInternalStatus = + existingActivity.amInternalStatus; + amDisplayStatus = + existingActivity.amDisplayStatus; + } else { + activityInternalStatus = + ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; + activityDisplayStatus = + ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; + amInternalStatus = + ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; + amDisplayStatus = + ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; } + } else { + if (isDraft) { + activityInternalStatus = + ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT; + activityDisplayStatus = + ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT; + amInternalStatus = + ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_DRAFT; + amDisplayStatus = + ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_DRAFT; + } else { + activityInternalStatus = + ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; + activityDisplayStatus = + ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; + amInternalStatus = + ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; + amDisplayStatus = + ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; + } + } - // Booleans (assign explicitly for type-safety) - if (payload.foodAvailable !== undefined) activityData.foodAvailable = payload.foodAvailable; - if (payload.foodIsChargeable !== undefined) activityData.foodIsChargeable = payload.foodIsChargeable; - if (payload.alcoholAvailable !== undefined) activityData.alcoholAvailable = payload.alcoholAvailable; - if (payload.trainerAvailable !== undefined) activityData.trainerAvailable = payload.trainerAvailable; - if (payload.trainerIsChargeable !== undefined) activityData.trainerIsChargeable = payload.trainerIsChargeable; - if (payload.pickUpDropAvailable !== undefined) activityData.pickUpDropAvailable = payload.pickUpDropAvailable; - if (payload.pickUpDropIsChargeable !== undefined) activityData.pickUpDropIsChargeable = payload.pickUpDropIsChargeable; - if (payload.inActivityAvailable !== undefined) activityData.inActivityAvailable = payload.inActivityAvailable; - if (payload.inActivityIsChargeable !== undefined) activityData.inActivityIsChargeable = payload.inActivityIsChargeable; - if (payload.equipmentAvailable !== undefined) activityData.equipmentAvailable = payload.equipmentAvailable; - if (payload.equipmentIsChargeable !== undefined) activityData.equipmentIsChargeable = payload.equipmentIsChargeable; - if (payload.cancellationAvailable !== undefined) activityData.cancellationAvailable = payload.cancellationAvailable; - if (payload.isInstantBooking !== undefined) activityData.isInstantBooking = payload.isInstantBooking; + /* -------------------------------- + * 4️⃣ UPDATE ACTIVITY CORE + FLAGS + * -------------------------------- */ + const activity = await tx.activities.update({ + where: { id: existingActivity.id }, + data: { + activityTitle: payload.activityTitle ?? undefined, + activityDescription: payload.activityDescription ?? undefined, + currencyXid: payload.currencyXid ?? undefined, + isInstantBooking: payload.isInstantBooking ?? undefined, - if (payload.currencyXid) activityData.currencyXid = Number(payload.currencyXid); - if (payload.sustainabilityScore !== undefined && payload.sustainabilityScore !== null) activityData.sustainabilityScore = Number(payload.sustainabilityScore); - if (payload.safetyScore !== undefined && payload.safetyScore !== null) activityData.safetyScore = Number(payload.safetyScore); + foodAvailable: payload.foodAvailable, + alcoholAvailable: payload.alcoholAvailable, + trainerAvailable: payload.trainerAvailable, + pickUpDropAvailable: payload.pickUpDropAvailable, + inActivityAvailable: payload.inActivityAvailable, + equipmentAvailable: payload.equipmentAvailable, + cancellationAvailable: payload.cancellationAvailable, + isCheckOutSame: payload.isCheckOutSame, - // Create activity - const createdActivity = await tx.activities.create({ data: activityData }); + activityInternalStatus, + activityDisplayStatus, + amInternalStatus, + amDisplayStatus, + }, + }); - // Other details (nested DTO) - const od = (payload as any).otherDetails; - if (od && (od.exclusiveNotes || od.dosNotes || od.dontsNotes || od.tipsNotes || od.termsAndCondition)) { - await tx.activityOtherDetails.create({ - data: { - activityXid: createdActivity.id, - exclusiveNotes: od.exclusiveNotes ? String(od.exclusiveNotes).substring(0,50) : null, - dosNotes: od.dosNotes ? String(od.dosNotes).substring(0,200) : null, - dontsNotes: od.dontsNotes ? String(od.dontsNotes).substring(0,200) : null, - tipsNotes: od.tipsNotes ? String(od.tipsNotes).substring(0,100) : null, - termsAndCondition: od.termsAndCondition ? String(od.termsAndCondition).substring(0,500) : null, - } + const activityXid = activity.id; + + /* -------------------------------- + * 5️⃣ CLEAN OLD VENUES + * -------------------------------- */ + const oldVenueIds = ( + await tx.activityVenues.findMany({ + where: { activityXid }, + select: { id: true }, + }) + ).map(v => v.id); + + if (oldVenueIds.length) { + const priceIds = ( + await tx.activityPrices.findMany({ + where: { activityVenueXid: { in: oldVenueIds } }, + select: { id: true }, + }) + ).map(p => p.id); + + if (priceIds.length) { + await tx.activityPriceTaxes.deleteMany({ + where: { activityPriceXid: { in: priceIds } }, + }); + await tx.activityPrices.deleteMany({ + where: { id: { in: priceIds } }, }); } - // Media - if (Array.isArray(payload.media) && payload.media.length) { - const mediaData = payload.media.map((m: any, idx: number) => ({ - activityXid: createdActivity.id, - mediaType: String(m.mediaType || 'image'), - mediaFileName: String(m.mediaFileName), - displayOrder: idx + 1, - })); - await tx.activitiesMedia.createMany({ data: mediaData }); - } + await tx.activityVenues.deleteMany({ + where: { id: { in: oldVenueIds } }, + }); + } - // Venues + Prices - if (Array.isArray(payload.venues) && payload.venues.length) { - for (const venue of payload.venues) { - const v = await tx.activityVenues.create({ - data: { - activityXid: createdActivity.id, - venueName: String(venue.venueName).substring(0,50), - venueCapacity: venue.venueCapacity ? Number(venue.venueCapacity) : 0, - availableSeats: venue.availableSeats ? Number(venue.availableSeats) : 0, - isMinPeopleReqMandatory: Boolean(venue.isMinPeopleReqMandatory || false), - minPeopleRequired: venue.minPeopleRequired ? Number(venue.minPeopleRequired) : null, - minReqfullfilledBeforeMins: venue.minReqfullfilledBeforeMins ? Number(venue.minReqfullfilledBeforeMins) : null, - venueDescription: venue.venueDescription ? String(venue.venueDescription).substring(0,200) : null, - } - }); - - if (Array.isArray(venue.prices) && venue.prices.length) { - const pricesData = venue.prices.map((p: any) => ({ - activityVenueXid: v.id, - noOfSession: Number(p.noOfSession || 1), - isPackage: Boolean(p.isPackage || false), - sessionValidity: Number(p.sessionValidity || 0), - sessionValidityFrequency: String(p.sessionValidityFrequency || 'Days'), - basePrice: Number(p.basePrice || 0), - sellPrice: Number(p.sellPrice || 0), - })); - await tx.activityPrices.createMany({ data: pricesData }); - } - } - } - - // Food types - if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) { - const ft = payload.foodTypeIds.map((id: any) => ({ activityXid: createdActivity.id, foodTypeXid: Number(id) })); - await tx.activityFoodTypes.createMany({ data: ft }); - } - - // Cuisines - if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) { - const cs = payload.cuisineIds.map((id: any) => ({ activityXid: createdActivity.id, foodCuisineXid: Number(id) })); - await tx.activityCuisine.createMany({ data: cs }); - } - - // Pick up transport + details - if (Array.isArray(payload.pickupTransports) && payload.pickupTransports.length) { - for (const pt of payload.pickupTransports) { - const transport = await tx.activityPickUpTransport.create({ - data: { - activityXid: createdActivity.id, - transportModeXid: Number(pt.transportModeXid), - isTransportModeChargeable: Boolean(pt.isTransportModeChargeable || false), - } - }); - - if (Array.isArray(pt.pickupDetails) && pt.pickupDetails.length) { - const pd = pt.pickupDetails.map((d: any) => ({ - activityPickUpTransportXid: transport.id, - isPickUp: Boolean(d.isPickUp || false), - locationLat: d.locationLat ? Number(d.locationLat) : null, - locationLong: d.locationLong ? Number(d.locationLong) : null, - locationAddress: d.locationAddress ? String(d.locationAddress).substring(0,150) : null, - transportBasePrice: d.transportBasePrice ? Number(d.transportBasePrice) : 0, - transportTotalPrice: d.transportTotalPrice ? Number(d.transportTotalPrice) : 0, - })); - await tx.activityPickUpDetails.createMany({ data: pd }); - } - } - } - - // Navigation modes - if (Array.isArray(payload.navigationModes) && payload.navigationModes.length) { - const navs = payload.navigationModes.map((n: any) => ({ - activityXid: createdActivity.id, - navigationModeXid: Number(n.navigationModeXid), - isInActivityChargeable: Boolean(n.isInActivityChargeable || false), - navigationModesBasePrice: Number(n.basePrice || 0), - navigationModesTotalPrice: Number(n.totalPrice || 0), - })); - await tx.activityNavigationModes.createMany({ data: navs }); - } - - // Equipments - if (Array.isArray(payload.equipments) && payload.equipments.length) { - const eqs = payload.equipments.map((e: any) => ({ - activityXid: createdActivity.id, - equipmentName: String(e.equipmentName).substring(0,30), - isEquipmentChargeable: Boolean(e.isEquipmentChargeable || false), - equipmentBasePrice: Number(e.equipmentBasePrice || 0), - equipmentTotalPrice: Number(e.equipmentTotalPrice || 0), - })); - await tx.activityEquipments.createMany({ data: eqs }); - } - - // Amenities - if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) { - const ams = payload.amenitiesIds.map((id: any) => ({ activityXid: createdActivity.id, amenitiesXid: Number(id) })); - await tx.activityAmenities.createMany({ data: ams }); - } - - // Eligibility - if (payload.eligibility) { - const e = payload.eligibility; - await tx.activityEligibility.create({ - data: { - activityXid: createdActivity.id, - isAgeRestriction: Boolean(e.isAgeRestriction || false), - ageRestrictionXid: e.ageRestrictionXid ? Number(e.ageRestrictionXid) : null, - isWeightRestriction: Boolean(e.isWeightRestriction || false), - weightRestrictionName: e.weightRestrictionName ? String(e.weightRestrictionName).substring(0,30) : null, - weightEntered: e.weightEntered ? Number(e.weightEntered) : null, - weightIn: e.weightIn ? String(e.weightIn).substring(0,30) : null, - minWeight: e.minWeight ? Number(e.minWeight) : null, - maxWeight: e.maxWeight ? Number(e.maxWeight) : null, - isHeightRestriction: Boolean(e.isHeightRestriction || false), - heightRestrictionName: e.heightRestrictionName ? String(e.heightRestrictionName).substring(0,30) : null, - heightEntered: e.heightEntered ? Number(e.heightEntered) : null, - minHeight: e.minHeight ? Number(e.minHeight) : null, - maxHeight: e.maxHeight ? Number(e.maxHeight) : null, - } - }); - } - - // ----------------- PQQ seeding (copy of existing logic) ----------------- - const questions = await tx.pQQCategories.findMany({ - where: { isActive: true }, - select: { - id: true, - categoryName: true, - displayOrder: true, - pqqsubCategories: { - where: { isActive: true }, - select: { - id: true, - subCategoryName: true, - displayOrder: true, - questions: { - where: { isActive: true }, - select: { - id: true, - questionName: true, - maxPoints: true, - displayOrder: true, - }, - orderBy: { displayOrder: 'asc' }, - }, - }, - orderBy: { displayOrder: 'asc' }, - }, + /* -------------------------------- + * 6️⃣ CREATE VENUES (MULTIPLE) + * -------------------------------- */ + for (const venue of payload.venues ?? []) { + const venueRow = await tx.activityVenues.create({ + data: { + activityXid, + venueName: venue.venueName, + venueCapacity: venue.venueCapacity ?? 0, + availableSeats: venue.availableSeats ?? 0, + isMinPeopleReqMandatory: venue.isMinPeopleReqMandatory, + minPeopleRequired: venue.minPeopleRequired ?? null, + minReqfullfilledBeforeMins: + venue.minReqfullfilledBeforeMins ?? null, }, - orderBy: { displayOrder: 'asc' }, }); - const allQuestions: number[] = []; - for (const cat of questions) { - for (const sub of cat.pqqsubCategories) { - for (const q of sub.questions) { - allQuestions.push(q.id); - } + for (const price of venue.prices) { + const sellPrice = Number(price.sellPrice); + const { basePrice, taxDetails } = + computeBasePriceAndTaxes(sellPrice, rootTaxes); + + const priceRow = await tx.activityPrices.create({ + data: { + activityVenueXid: venueRow.id, + noOfSession: price.noOfSession ?? 1, + isPackage: price.isPackage ?? false, + sessionValidity: price.sessionValidity ?? 0, + sessionValidityFrequency: + price.sessionValidityFrequency ?? 'Days', + basePrice, + sellPrice, + }, + }); + + if (taxDetails.length) { + await tx.activityPriceTaxes.createMany({ + data: taxDetails.map(t => ({ + activityPriceXid: priceRow.id, + taxXid: t.taxXid, + taxPer: t.taxPer, + taxAmount: t.taxAmount, + })), + }); } } + } - if (allQuestions.length) { - await tx.activityPQQheader.createMany({ - data: allQuestions.map((id) => ({ - activityXid: createdActivity.id, - pqqQuestionXid: id, - pqqAnswerXid: null, - })), + /* -------------------------------- + * 7️⃣ TRAINER + * -------------------------------- */ + if (payload.trainerAvailable) { + const { basePrice, taxDetails } = + computeBasePriceAndTaxes( + payload.trainerTotalAmount, + rootTaxes, + ); + + const trainer = await tx.activityTrainers.create({ + data: { + activityXid, + baseAmount: basePrice, + totalAmount: payload.trainerTotalAmount, + }, + }); + + for (const t of taxDetails) { + await tx.activityTrainerTaxes.create({ + data: { + activityTrainerXid: trainer.id, + taxXid: t.taxXid, + taxPer: t.taxPer, + taxAmount: t.taxAmount, + }, }); } + } - return { activity_xid: createdActivity.id }; + /* -------------------------------- + * 8️⃣ ACTIVITY TRACK + * -------------------------------- */ + await tx.activityTrack.create({ + data: { + activityXid, + trackType: 'ACTIVITY', + trackStatus: activityInternalStatus, + updatedByXid: userId, + updatedByRole: ROLE_NAME.HOST, + updatedOn: new Date(), + }, }); - } + + /* -------------------------------- + * 9️⃣ RESPONSE + * -------------------------------- */ + return { + activityXid, + activityRefNumber: activity.activityRefNumber, + status: isDraft + ? 'ACTIVITY_SAVED_AS_DRAFT' + : 'ACTIVITY_SUBMITTED', + }; + }); +} async getAllPQUpdatedResponse(activityXid: number) { From f13e90ce39abe632294c0617310b0fd1f3a8b63f Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Mon, 22 Dec 2025 13:30:55 +0530 Subject: [PATCH 23/24] upated createNewActivity handler --- prisma/schema.prisma | 8 +- src/modules/host/dto/createActivity.schema.ts | 28 +- .../OnBoarding/CreateNewActivity.ts | 89 +- src/modules/host/services/host.service.ts | 970 +++++++++++++----- 4 files changed, 782 insertions(+), 313 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ca8a446..56b18b8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -861,7 +861,7 @@ model Activities { frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict) activityRefNumber String? @map("activity_ref_number") @db.VarChar(30) activityTitle String? @map("activity_title") @db.VarChar(30) - activityDescription String? @map("activity_description") @db.VarChar(80) + activityDescription String? @map("activity_description") @db.VarChar(255) checkInLat Float? @map("check_in_lat") checkInLong Float? @map("check_in_long") checkInAddress String? @map("check_in_address") @db.VarChar(150) @@ -913,7 +913,6 @@ model Activities { ActivityEligibility ActivityEligibility[] ActivitySuggestions ActivitySuggestions[] ActivityAmDetails ActivityAmDetails[] - ActivityVenueArtifacts ActivityVenueArtifacts[] ActivityPQQheader ActivityPQQheader[] ActivityAllowedEntry ActivityAllowedEntry[] ActivityFoodCost ActivityFoodCost[] @@ -936,7 +935,7 @@ model ActivityOtherDetails { id Int @id @default(autoincrement()) activityXid Int @map("activity_xid") activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) - exclusiveNotes String? @map("exclusive_notes") @db.VarChar(50) + exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500) dosNotes String? @map("dos_notes") @db.VarChar(200) dontsNotes String? @map("donts_notes") @db.VarChar(200) tipsNotes String? @map("tips_notes") @db.VarChar(100) @@ -1003,6 +1002,7 @@ model ActivityVenues { ScheduleHeader ScheduleHeader[] ItineraryActivities ItineraryActivities[] ActivityPrices ActivityPrices[] // <-- Added opposite relation + ActivityVenueArtifacts ActivityVenueArtifacts[] // <-- Added opposite relation @@map("activity_venues") @@schema("act") @@ -1143,7 +1143,7 @@ model ActivityPriceTaxes { model ActivityVenueArtifacts { id Int @id @default(autoincrement()) activityVenueXid Int @map("activity_venue_xid") - activityVenue Activities @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade) + activityVenue ActivityVenues @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade) mediaType String @map("media_type") @db.VarChar(30) mediaFileName String @map("media_file_name") @db.VarChar(400) isActive Boolean @default(true) @map("is_active") diff --git a/src/modules/host/dto/createActivity.schema.ts b/src/modules/host/dto/createActivity.schema.ts index 5032530..98d2874 100644 --- a/src/modules/host/dto/createActivity.schema.ts +++ b/src/modules/host/dto/createActivity.schema.ts @@ -2,12 +2,12 @@ import { z } from 'zod'; /* ================= MEDIA ================= */ export const MediaDto = z.object({ - mediaType: z.string().optional(), - mediaFileName: z.string(), + mediaType: z.string().optional(), // "image/jpeg", "video/mp4", etc. + mediaFileName: z.string(), // S3 file URL }); /* ================= PRICE ================= - * ❌ NO TAX HERE (tax is root-level only) + * ❌ No tax info here; root-level only */ export const PriceDto = z.object({ noOfSession: z.number().int().optional().default(1), @@ -15,7 +15,7 @@ export const PriceDto = z.object({ sessionValidity: z.number().int().optional().default(0), sessionValidityFrequency: z.string().optional().default('Days'), basePrice: z.number().int().optional().default(0), - sellPrice: z.number().int(), // REQUIRED + sellPrice: z.number().int(), // required }); /* ================= VENUE ================= */ @@ -27,6 +27,11 @@ export const VenueDto = z.object({ minPeopleRequired: z.number().int().nullable().optional(), minReqfullfilledBeforeMins: z.number().int().nullable().optional(), venueDescription: z.string().optional(), + + // ✅ new: media per venue (for ActivityVenueArtifacts) + media: z.array(MediaDto).optional().default([]), + + // price list per venue prices: z.array(PriceDto).optional().default([]), }); @@ -69,6 +74,7 @@ export const EligibilityDto = z.object({ isHeightRestriction: z.boolean().optional().default(false), heightRestrictionName: z.string().nullable().optional(), heightEntered: z.number().int().nullable().optional(), + heightIn: z.string().nullable().optional(), minHeight: z.number().int().nullable().optional(), maxHeight: z.number().int().nullable().optional(), }); @@ -127,7 +133,7 @@ export const CreateActivityDto = z.object({ cancellationAvailable: z.boolean().optional().default(false), - /* MONEY */ + /* MONEY / CURRENCY */ currencyXid: z.number().int().nullable().optional(), sustainabilityScore: z.number().int().nullable().optional(), safetyScore: z.number().int().nullable().optional(), @@ -136,21 +142,19 @@ export const CreateActivityDto = z.object({ /* 🔥 ROOT-LEVEL TAX (SINGLE SOURCE OF TRUTH) */ taxXids: z.array(z.number().int()).optional().default([]), - /* ARRAYS */ - media: z.array(MediaDto).optional().default([]), - venues: z.array(VenueDto).optional().default([]), + /* 🔥 MEDIA ARRAYS */ + media: z.array(MediaDto).optional().default([]), // Activity-level media + venues: z.array(VenueDto).optional().default([]), // Each venue’s media + prices + /* RELATION ARRAYS */ foodTypeIds: z.array(z.number().int()).optional().default([]), cuisineIds: z.array(z.number().int()).optional().default([]), - pickupTransports: z.array(PickupTransportDto).optional().default([]), - - /* 🔥 NAVIGATION = IDs ONLY */ navigationModes: z.array(z.number().int()).optional().default([]), - equipments: z.array(EquipmentDto).optional().default([]), amenitiesIds: z.array(z.number().int()).optional().default([]), + /* EXTRA OBJECTS */ eligibility: EligibilityDto.optional(), otherDetails: OtherDetailsDto.optional(), }); diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts index e6edb29..cd77d0e 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -22,7 +22,6 @@ function getExtensionFromMime(mimeType: string) { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', - // ✅ Common video formats 'video/mp4': 'mp4', 'video/quicktime': 'mov', 'video/x-msvideo': 'avi', @@ -141,13 +140,21 @@ export const handler = safeHandler( 'currencyXid', 'energyLevelXid', 'activityDurationMins', + 'activityTypeXid', + 'frequenciesXid', 'trainerTotalAmount', 'pickupDropTotalPrice', 'navigationModeTotalPrice', + 'sustainabilityScore', + 'safetyScore', + 'checkInLat', + 'checkInLong', + 'checkOutLat', + 'checkOutLong', ]; for (const key of numberKeys) { - if (activityPayload[key] !== undefined && activityPayload[key] !== null) { + if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') { activityPayload[key] = Number(activityPayload[key]); } } @@ -175,17 +182,13 @@ export const handler = safeHandler( if (activityPayload[key] === 'false') activityPayload[key] = false; } - /* 8️⃣ UPLOAD MEDIA */ - const uploadedMedia: Array<{ mediaType?: string; mediaFileName: string }> = - []; + /* 8️⃣ UPLOAD ACTIVITY-LEVEL MEDIA (images/videos) */ + const uploadedActivityMedia: Array<{ mediaType?: string; mediaFileName: string }> = []; - // ✅ Accept both images and videos under multipart fields `images` or `videos` for (const file of files.filter( - (f) => f.fieldName === 'images' || f.fieldName === 'videos', + (f) => f.fieldName === 'activityImages' || f.fieldName === 'activityVideos', )) { - const ext = getExtensionFromMime(file.mimeType); - - const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Artifacts/${file.fileName}`; + const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Media/${Date.now()}_${file.fileName}`; if (s3Key.length > 900) { throw new ApiError(400, 'Generated S3 key too long'); @@ -201,20 +204,72 @@ export const handler = safeHandler( }) .promise(); - uploadedMedia.push({ + uploadedActivityMedia.push({ mediaType: file.mimeType, mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, }); } - /* 🔥 MERGE MEDIA (DO NOT OVERWRITE) */ + /* 🔥 MERGE ACTIVITY MEDIA */ const existingMedia = Array.isArray(activityPayload.media) ? activityPayload.media : []; + activityPayload.media = [...existingMedia, ...uploadedActivityMedia]; - activityPayload.media = [...existingMedia, ...uploadedMedia]; + /* 9️⃣ PROCESS VENUE MEDIA UPLOADS */ + // Group venue files by index: venueImages[0], venueImages[1], etc. + const venueFilesMap: Map> = new Map(); - /* 9️⃣ VALIDATION */ + for (const file of files) { + // Match patterns like: venueImages[0], venueVideos[1], etc. + const match = file.fieldName.match(/^venue(Images|Videos)\[(\d+)\]$/); + if (match) { + const venueIndex = parseInt(match[2], 10); + if (!venueFilesMap.has(venueIndex)) { + venueFilesMap.set(venueIndex, []); + } + venueFilesMap.get(venueIndex)!.push(file); + } + } + + // Upload venue files and attach to corresponding venues + if (Array.isArray(activityPayload.venues)) { + for (let i = 0; i < activityPayload.venues.length; i++) { + const venue = activityPayload.venues[i]; + const venueFiles = venueFilesMap.get(i) || []; + + const uploadedVenueMedia: Array<{ mediaType?: string; mediaFileName: string }> = []; + + for (const file of venueFiles) { + const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Venue_${i}/Media/${Date.now()}_${file.fileName}`; + + if (s3Key.length > 900) { + throw new ApiError(400, 'Generated S3 key too long for venue media'); + } + + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: s3Key, + Body: file.buffer, + ContentType: file.mimeType, + ACL: 'private', + }) + .promise(); + + uploadedVenueMedia.push({ + mediaType: file.mimeType, + mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, + }); + } + + // Merge with existing venue media + const existingVenueMedia = Array.isArray(venue.media) ? venue.media : []; + venue.media = [...existingVenueMedia, ...uploadedVenueMedia]; + } + } + + /* 🔟 VALIDATION */ let parsedDto: CreateActivityInput; if (!isDraft) { @@ -230,14 +285,14 @@ export const handler = safeHandler( parsedDto = activityPayload as CreateActivityInput; } - /* 🔟 SAVE ACTIVITY */ + /* 1️⃣1️⃣ SAVE ACTIVITY */ const createdActivity = await hostService.createOrUpdateActivity( userInfo.id, parsedDto, isDraft, ); - /* 1️⃣1️⃣ RESPONSE */ + /* 1️⃣2️⃣ RESPONSE */ return { statusCode: 200, headers: { @@ -253,4 +308,4 @@ export const handler = safeHandler( }), }; }, -); +); \ No newline at end of file diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index acbd213..20a9266 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -14,7 +14,9 @@ import { hostCompanyDetailsSchema } from '../../../common/utils/validation/host/ import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, - ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, HOST_STATUS_DISPLAY, + ACTIVITY_DISPLAY_STATUS, + ACTIVITY_INTERNAL_STATUS, + HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, STEPPER, } from '../../../common/utils/constants/host.constant'; @@ -40,7 +42,6 @@ function sanitizeDocumentName(name?: string) { .substring(0, 100); } - type HostCompanyDetailsInput = z.infer; // Document input after S3 upload (with S3 URL as filePath) @@ -61,7 +62,7 @@ export async function generateActivityRefNumber(tx: any) { const nextId = lastrecord ? lastrecord.id + 1 : 1; - return `ACT-${String(nextId).padStart(6, '0')}`;; + return `ACT-${String(nextId).padStart(6, '0')}`; } function round2(value: number) { @@ -75,13 +76,21 @@ function computeBasePriceAndTaxes( if (!taxes?.length) { return { basePrice: round2(sellPrice), - taxDetails: [] as Array<{ taxXid: number; taxPer: number; taxAmount: number }>, + taxDetails: [] as Array<{ + taxXid: number; + taxPer: number; + taxAmount: number; + }>, }; } - const totalTaxPer = taxes.reduce((sum, t) => sum + (Number(t.taxPer) || 0), 0); + const totalTaxPer = taxes.reduce( + (sum, t) => sum + (Number(t.taxPer) || 0), + 0, + ); const denominator = 1 + totalTaxPer / 100; - const basePrice = denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice); + const basePrice = + denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice); const taxDetails = taxes.map((t) => ({ taxXid: t.id, @@ -96,7 +105,7 @@ const bucket = config.aws.bucketName; @Injectable() export class HostService { - constructor(private prisma: PrismaClient) { } + constructor(private prisma: PrismaClient) {} async createHost(data: CreateHostDto) { return this.prisma.user.create({ data }); @@ -115,7 +124,7 @@ export class HostService { const user = await this.prisma.user.findUnique({ where: { id: user_xid }, select: { id: true, emailAddress: true }, - }) + }); return { host, user }; } @@ -131,10 +140,10 @@ export class HostService { filePath: true, documentName: true, documentTypeXid: true, - documentType: true - } - } - } + documentType: true, + }, + }, + }, }, HostBankDetails: true, HostDocuments: { @@ -151,7 +160,7 @@ export class HostService { mobileNumber: true, profileImage: true, userRefNumber: true, - } + }, }, user: { select: { @@ -163,7 +172,7 @@ export class HostService { profileImage: true, userStatus: true, userRefNumber: true, - } + }, }, companyTypes: { select: { @@ -182,7 +191,7 @@ export class HostService { title: true, comments: true, isparent: true, - } + }, }, countries: true, currencies: true, @@ -198,7 +207,6 @@ export class HostService { } if (host.HostDocuments?.length) { - for (const doc of host.HostDocuments) { if (doc.filePath) { const filePath = doc.filePath; @@ -213,8 +221,8 @@ export class HostService { } } if (host.user?.profileImage) { - const key = host.user.profileImage.startsWith("http") - ? host.user.profileImage.split(".com/")[1] + const key = host.user.profileImage.startsWith('http') + ? host.user.profileImage.split('.com/')[1] : host.user.profileImage; host.user.profileImage = await getPresignedUrl(bucket, key); @@ -241,8 +249,8 @@ export class HostService { // Parent company logo if (parent.logoPath) { - const key = parent.logoPath.startsWith("http") - ? parent.logoPath.split(".com/")[1] + const key = parent.logoPath.startsWith('http') + ? parent.logoPath.split('.com/')[1] : parent.logoPath; parent.logoPath = await getPresignedUrl(bucket, key); @@ -252,8 +260,8 @@ export class HostService { if (parent.HostParenetDocuments?.length) { for (const doc of parent.HostParenetDocuments) { if (doc.filePath) { - const key = doc.filePath.startsWith("http") - ? doc.filePath.split(".com/")[1] + const key = doc.filePath.startsWith('http') + ? doc.filePath.split('.com/')[1] : doc.filePath; (doc as any).presignedUrl = await getPresignedUrl(bucket, key); @@ -337,15 +345,18 @@ export class HostService { emailAddress: true, mobileNumber: true, userPassword: true, - userStatus: true - } + userStatus: true, + }, }); if (!existingUser) { throw new ApiError(404, 'User not found'); } if (existingUser.userStatus == USER_STATUS.REJECTED) { - throw new ApiError(403, "You are not allowed to login. Please contact minglar admin.") + throw new ApiError( + 403, + 'You are not allowed to login. Please contact minglar admin.', + ); } if (existingUser.roleXid !== 4) { @@ -429,7 +440,7 @@ export class HostService { if (existingAccount) { throw new ApiError( 400, - 'Host account with this account number already exists.' + 'Host account with this account number already exists.', ); } const addedPaymentDetails = await tx.hostBankDetails.create({ @@ -444,16 +455,20 @@ export class HostService { where: { id: data.hostXid }, data: { stepper: STEPPER.BANK_DETAILS_UPDATED, - currencyXid: data.currencyXid + currencyXid: data.currencyXid, }, }); }); } - async getAllHostActivity(search?: string, user_xid?: number, paginationOptions?: { page: number; limit: number; skip: number }) { + async getAllHostActivity( + search?: string, + user_xid?: number, + paginationOptions?: { page: number; limit: number; skip: number }, + ) { const hostDetails = await this.prisma.hostHeader.findFirst({ - where: { userXid: user_xid, isActive: true } - }) + where: { userXid: user_xid, isActive: true }, + }); const whereClause: any = { isActive: true, @@ -464,7 +479,7 @@ export class HostService { data: [], total: 0, page: paginationOptions?.page || 1, - limit: paginationOptions?.limit || 10 + limit: paginationOptions?.limit || 10, }; } @@ -477,8 +492,8 @@ export class HostService { { activityTitle: { contains: term, mode: 'insensitive' } }, { activityType: { - activityTypeName: { contains: term, mode: 'insensitive' } - } + activityTypeName: { contains: term, mode: 'insensitive' }, + }, }, ]; } @@ -500,8 +515,8 @@ export class HostService { frequency: { select: { id: true, - frequencyName: true - } + frequencyName: true, + }, }, ActivityAmDetails: { select: { @@ -524,10 +539,10 @@ export class HostService { interests: { select: { id: true, - interestName: true - } - } - } + interestName: true, + }, + }, + }, }, }, skip: paginationOptions?.skip || 0, @@ -542,8 +557,8 @@ export class HostService { const am = activity.ActivityAmDetails?.[0]?.accountManager; if (am?.profileImage) { - const key = am.profileImage.startsWith("http") - ? am.profileImage.split(".com/")[1] + const key = am.profileImage.startsWith('http') + ? am.profileImage.split('.com/')[1] : am.profileImage; const presignedUrl = await getPresignedUrl(bucket, key); @@ -555,11 +570,13 @@ export class HostService { } } - const { paginationService } = require('@/common/utils/pagination/pagination.service'); + const { + paginationService, + } = require('@/common/utils/pagination/pagination.service'); return paginationService.createPaginatedResponse( hostAllActivities, totalCount, - paginationOptions || { page: 1, limit: 10, skip: 0 } + paginationOptions || { page: 1, limit: 10, skip: 0 }, ); } @@ -596,8 +613,8 @@ export class HostService { id: true, activityPqqHeaderXid: true, mediaFileName: true, - mediaType: true - } + mediaType: true, + }, }, ActivityPQQSuggestions: { where: { isActive: true, isReviewed: false }, @@ -605,13 +622,12 @@ export class HostService { id: true, title: true, comments: true, - } + }, }, }, }); if (detailsOfQuestion.ActivityPQQSupportings?.length) { - for (const doc of detailsOfQuestion.ActivityPQQSupportings) { if (doc.mediaFileName) { const filePath = doc.mediaFileName; @@ -635,8 +651,8 @@ export class HostService { activityXid: activity_xid, isActive: true, pqqAnswerXid: { - not: null - } + not: null, + }, }, select: { pqqQuestionXid: true, @@ -669,10 +685,9 @@ export class HostService { include: { HostParenetDocuments: true }, }); - return parents.flatMap(p => p.HostParenetDocuments); + return parents.flatMap((p) => p.HostParenetDocuments); } - async deleteExistingParentRecords(userId: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: userId }, @@ -688,7 +703,7 @@ export class HostService { if (!parents.length) return; - const parentIds = parents.map(p => p.id); + const parentIds = parents.map((p) => p.id); // 1️⃣ Delete documents first await this.prisma.hostParenetDocuments.deleteMany({ @@ -701,7 +716,6 @@ export class HostService { }); } - async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, @@ -716,7 +730,7 @@ export class HostService { where: { userXid: user_xid }, include: { hostParent: true }, }); - console.log(existingHostCompany, "-: Existing hai") + console.log(existingHostCompany, '-: Existing hai'); let existingParentCompany; @@ -725,9 +739,9 @@ export class HostService { where: { hostXid: existingHostCompany.id }, select: { id: true, - logoPath: true - } - }) + logoPath: true, + }, + }); } let hostStatusInternal; @@ -745,7 +759,8 @@ export class HostService { // CASE 1: Host was asked to update AND is submitting final if ( existingHostCompany && - existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && + existingHostCompany.hostStatusInternal === + HOST_STATUS_INTERNAL.HOST_TO_UPDATE && !isDraft ) { hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED; @@ -757,7 +772,8 @@ export class HostService { // CASE 2: Host was asked to update BUT saving draft else if ( existingHostCompany && - existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && + existingHostCompany.hostStatusInternal === + HOST_STATUS_INTERNAL.HOST_TO_UPDATE && isDraft ) { // keep original @@ -792,14 +808,17 @@ export class HostService { // ------------------------------------------------------- if (!existingHostCompany) { if (!isDraft) { - console.log("First time direct final submit.") + console.log('First time direct final submit.'); const existingByPan = await tx.hostHeader.findFirst({ where: { panNumber: companyData.panNumber }, }); if (existingByPan) - throw new ApiError(400, 'Company already exists with this pan/bin number'); + throw new ApiError( + 400, + 'Company already exists with this pan/bin number', + ); } - console.log("First Time Aaya hai") + console.log('First Time Aaya hai'); const createdHost = await tx.hostHeader.create({ data: { @@ -807,9 +826,15 @@ export class HostService { companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, - cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, - states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, - countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, + cities: companyData.cityXid + ? { connect: { id: companyData.cityXid } } + : undefined, + states: companyData.stateXid + ? { connect: { id: companyData.stateXid } } + : undefined, + countries: companyData.countryXid + ? { connect: { id: companyData.countryXid } } + : undefined, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, @@ -849,7 +874,7 @@ export class HostService { // parent create if (companyData.isSubsidairy && parentCompanyData) { - console.log("Parent ke saath aaya hai first time.") + console.log('Parent ke saath aaya hai first time.'); const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: createdHost.id } }, @@ -857,17 +882,23 @@ export class HostService { address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, // Safely handle city connection - only connect if valid ID exists - cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) - ? { connect: { id: Number(parentCompanyData.cityXid) } } - : undefined, + cities: + parentCompanyData?.cityXid && + !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } + : undefined, - states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) - ? { connect: { id: Number(parentCompanyData.stateXid) } } - : undefined, + states: + parentCompanyData?.stateXid && + !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } + : undefined, - countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) - ? { connect: { id: Number(parentCompanyData.countryXid) } } - : undefined, + countries: + parentCompanyData?.countryXid && + !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } + : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, @@ -922,19 +953,22 @@ export class HostService { address1: companyData.address1, address2: companyData.address2, // Safely handle city connection - only connect if valid ID exists - cities: companyData.cityXid && !isNaN(Number(companyData.cityXid)) - ? { connect: { id: Number(companyData.cityXid) } } - : undefined, // Don't change if not provided + cities: + companyData.cityXid && !isNaN(Number(companyData.cityXid)) + ? { connect: { id: Number(companyData.cityXid) } } + : undefined, // Don't change if not provided // Same for state - states: companyData.stateXid && !isNaN(Number(companyData.stateXid)) - ? { connect: { id: Number(companyData.stateXid) } } - : undefined, + states: + companyData.stateXid && !isNaN(Number(companyData.stateXid)) + ? { connect: { id: Number(companyData.stateXid) } } + : undefined, // Same for country - countries: companyData.countryXid && !isNaN(Number(companyData.countryXid)) - ? { connect: { id: Number(companyData.countryXid) } } - : undefined, + countries: + companyData.countryXid && !isNaN(Number(companyData.countryXid)) + ? { connect: { id: Number(companyData.countryXid) } } + : undefined, pinCode: companyData.pinCode, logoPath: companyData.logoPath || existingHostCompany.logoPath, isSubsidairy: companyData.isSubsidairy, @@ -977,7 +1011,9 @@ export class HostService { where: { id: existingDoc.id }, data: { filePath: doc.filePath, - documentName: sanitizeDocumentName(doc.documentName) || existingDoc.documentName, + documentName: + sanitizeDocumentName(doc.documentName) || + existingDoc.documentName, }, }); } else { @@ -996,29 +1032,40 @@ export class HostService { // parent logic untouched if (companyData.isSubsidairy) { const parentRecords = existingHostCompany.hostParent; - const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords; - console.log("Yaha aaya update in the apretn me") + const parentRecord = Array.isArray(parentRecords) + ? parentRecords[0] + : parentRecords; + console.log('Yaha aaya update in the apretn me'); if (!parentRecord) { - console.log("Parent record nahi mila to create kar raha hai.") + console.log('Parent record nahi mila to create kar raha hai.'); const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: updatedHost.id } }, companyName: parentCompanyData.companyName || null, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, - cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) - ? { connect: { id: Number(parentCompanyData.cityXid) } } - : undefined, + cities: + parentCompanyData?.cityXid && + !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } + : undefined, - states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) - ? { connect: { id: Number(parentCompanyData.stateXid) } } - : undefined, + states: + parentCompanyData?.stateXid && + !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } + : undefined, - countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) - ? { connect: { id: Number(parentCompanyData.countryXid) } } - : undefined, + countries: + parentCompanyData?.countryXid && + !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } + : undefined, pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData?.logoPath || existingParentCompany?.logoPath || null, + logoPath: + parentCompanyData?.logoPath || + existingParentCompany?.logoPath || + null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, @@ -1055,19 +1102,28 @@ export class HostService { companyName: parentCompanyData.companyName || null, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, - cities: parentCompanyData?.cityXid && !isNaN(Number(parentCompanyData.cityXid)) - ? { connect: { id: Number(parentCompanyData.cityXid) } } - : undefined, + cities: + parentCompanyData?.cityXid && + !isNaN(Number(parentCompanyData.cityXid)) + ? { connect: { id: Number(parentCompanyData.cityXid) } } + : undefined, - states: parentCompanyData?.stateXid && !isNaN(Number(parentCompanyData.stateXid)) - ? { connect: { id: Number(parentCompanyData.stateXid) } } - : undefined, + states: + parentCompanyData?.stateXid && + !isNaN(Number(parentCompanyData.stateXid)) + ? { connect: { id: Number(parentCompanyData.stateXid) } } + : undefined, - countries: parentCompanyData?.countryXid && !isNaN(Number(parentCompanyData.countryXid)) - ? { connect: { id: Number(parentCompanyData.countryXid) } } - : undefined, + countries: + parentCompanyData?.countryXid && + !isNaN(Number(parentCompanyData.countryXid)) + ? { connect: { id: Number(parentCompanyData.countryXid) } } + : undefined, pinCode: parentCompanyData.pinCode || null, - logoPath: parentCompanyData?.logoPath || existingParentCompany?.logoPath || null, + logoPath: + parentCompanyData?.logoPath || + existingParentCompany?.logoPath || + null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, @@ -1087,19 +1143,23 @@ export class HostService { if (parentDocuments?.length) { for (const doc of parentDocuments) { - const existingParentDoc = await tx.hostParenetDocuments.findFirst({ - where: { - hostParentXid: parentRecord.id, - documentTypeXid: doc.documentTypeXid, + const existingParentDoc = await tx.hostParenetDocuments.findFirst( + { + where: { + hostParentXid: parentRecord.id, + documentTypeXid: doc.documentTypeXid, + }, }, - }); + ); if (existingParentDoc) { await tx.hostParenetDocuments.update({ where: { id: existingParentDoc.id }, data: { filePath: doc.filePath, - documentName: sanitizeDocumentName(doc.documentName) || existingParentDoc.documentName, + documentName: + sanitizeDocumentName(doc.documentName) || + existingParentDoc.documentName, }, }); } else { @@ -1116,13 +1176,17 @@ export class HostService { } } } else { - console.log("Last ke else block me aaya hai") + console.log('Last ke else block me aaya hai'); const previousParent = existingHostCompany.hostParent; let prevParentId = null; if (Array.isArray(previousParent) && previousParent.length) { prevParentId = previousParent[0].id; - } else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) { + } else if ( + previousParent && + typeof previousParent === 'object' && + 'id' in previousParent + ) { prevParentId = previousParent.id; } @@ -1160,8 +1224,6 @@ export class HostService { }); } - - async getSuggestionDetails(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid, isActive: true }, @@ -1171,7 +1233,7 @@ export class HostService { id: true, emailAddress: true, firstName: true, - userRefNumber: true + userRefNumber: true, }, }, accountManager: { @@ -1331,7 +1393,9 @@ export class HostService { // Overall percent const overallPercentage = - totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0; + totalMaxPoints > 0 + ? round2((totalUserPoints / totalMaxPoints) * 100) + : 0; // ---------- 🔥 ONLY FIRST 2 CATEGORIES ---------- const categoryArray = Object.values(categories); @@ -1351,14 +1415,14 @@ export class HostService { await this.prisma.activities.update({ where: { - id: activityXid + id: activityXid, }, data: { totalScore: round2(overallPercentage), sustainabilityScore: round2(categoryWise.Sustainability), safetyScore: round2(categoryWise.Safety), - } - }) + }, + }); // Return final score object return { @@ -1384,10 +1448,7 @@ export class HostService { }); } - async findHeaderByCompositeKey( - activityXid: number, - pqqQuestionXid: number, - ) { + async findHeaderByCompositeKey(activityXid: number, pqqQuestionXid: number) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid, @@ -1396,7 +1457,11 @@ export class HostService { }); } - async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) { + async updateHeader( + headerId: number, + pqqAnswerXid: number, + comments?: string | null, + ) { return await this.prisma.activityPQQheader.update({ where: { id: headerId, @@ -1441,15 +1506,17 @@ export class HostService { activityDisplayStatus: true, activityInternalStatus: true, amInternalStatus: true, - amDisplayStatus: true - } - }) + amDisplayStatus: true, + }, + }); if (!activity) { - throw new ApiError(404, "Activity not found") + throw new ApiError(404, 'Activity not found'); } - if (activity.activityInternalStatus == ACTIVITY_INTERNAL_STATUS.PQ_TO_UPDATE) { + if ( + activity.activityInternalStatus == ACTIVITY_INTERNAL_STATUS.PQ_TO_UPDATE + ) { return await this.prisma.$transaction(async (tx) => { await this.prisma.activities.update({ where: { id: activity_xid }, @@ -1457,9 +1524,9 @@ export class HostService { activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQ_SUBMITTED, activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQ_TO_REVIEW, - amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.REVISED - } - }) + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.REVISED, + }, + }); await tx.activityTrack.create({ data: { @@ -1468,10 +1535,10 @@ export class HostService { trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED, updatedByXid: user_xid, updatedByRole: ROLE_NAME.HOST, - updatedOn: new Date() - } - }) - }) + updatedOn: new Date(), + }, + }); + }); } else { return await this.prisma.$transaction(async (tx) => { await this.prisma.activities.update({ @@ -1480,9 +1547,9 @@ export class HostService { activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQ_SUBMITTED, activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQ_TO_REVIEW, - amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW - } - }) + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW, + }, + }); await tx.activityTrack.create({ data: { @@ -1491,13 +1558,12 @@ export class HostService { trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED, updatedByXid: user_xid, updatedByRole: ROLE_NAME.HOST, - updatedOn: new Date() - } - }) - }) + updatedOn: new Date(), + }, + }); + }); } - }) - + }); } async updateSupportingFile( @@ -1525,20 +1591,24 @@ export class HostService { }); } - async markPQQSuggestionReviewed(user_xid: number, activityPqqHeaderXid: number, activityPQQSuggestionId: number) { + async markPQQSuggestionReviewed( + user_xid: number, + activityPqqHeaderXid: number, + activityPQQSuggestionId: number, + ) { return await this.prisma.activityPQQSuggestions.update({ where: { id: activityPQQSuggestionId, activityPqqHeaderXid: activityPqqHeaderXid, isActive: true, - isReviewed: false + isReviewed: false, }, data: { isReviewed: true, reviewedByXid: user_xid, - reviewedOn: new Date() - } - }) + reviewedOn: new Date(), + }, + }); } async getAllPQQQuesAndSubmittedAns(activity_xid: number) { @@ -1565,11 +1635,11 @@ export class HostService { id: true, categoryName: true, displayOrder: true, - } - } - } - } - } + }, + }, + }, + }, + }, }, ActivityPQQSuggestions: { select: { @@ -1579,22 +1649,22 @@ export class HostService { isReviewed: true, reviewedBy: true, reviewedOn: true, - } + }, }, pqqAnswers: { select: { id: true, displayOrder: true, answerName: true, - answerPoints: true - } + answerPoints: true, + }, }, ActivityPQQSupportings: { select: { id: true, mediaFileName: true, mediaType: true, - } + }, }, }, }); @@ -1637,9 +1707,7 @@ export class HostService { activityTypeXid: number, frequenciesXid?: number, ) { - return await this.prisma.$transaction(async (tx) => { - // Fetch host const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, @@ -1684,10 +1752,9 @@ export class HostService { async createActivityAndAllQuestionsEntry( userId: number, activityTypeXid: number, - frequenciesXid: number + frequenciesXid: number, ) { return await this.prisma.$transaction(async (tx) => { - const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, }); @@ -1790,10 +1857,10 @@ export class HostService { select: { id: true, categoryName: true, - displayOrder: true - } - } - } + displayOrder: true, + }, + }, + }, }, // 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION @@ -1803,11 +1870,11 @@ export class HostService { id: true, answerName: true, answerPoints: true, - displayOrder: true + displayOrder: true, }, - orderBy: { displayOrder: "asc" } - } - } + orderBy: { displayOrder: 'asc' }, + }, + }, }, ActivityPQQSuggestions: { where: { isActive: true }, @@ -1815,19 +1882,19 @@ export class HostService { id: true, title: true, comments: true, - activityPqqHeaderXid: true - } + activityPqqHeaderXid: true, + }, }, ActivityPQQSupportings: { where: { isActive: true }, select: { id: true, mediaType: true, - mediaFileName: true - } + mediaFileName: true, + }, }, }, - orderBy: { id: "asc" } + orderBy: { id: 'asc' }, }); // ---------------- GROUPING ------------------ @@ -1844,19 +1911,21 @@ export class HostService { id: cat.id, categoryName: cat.categoryName, displayOrder: cat.displayOrder, - pqqsubCategories: [] + pqqsubCategories: [], }; } const category = grouped[cat.id]; - let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id); + let subCat = category.pqqsubCategories.find( + (s: any) => s.id === sub.id, + ); if (!subCat) { subCat = { id: sub.id, subCategoryName: sub.subCategoryName, displayOrder: sub.displayOrder, - questions: [] + questions: [], }; category.pqqsubCategories.push(subCat); } @@ -1873,20 +1942,25 @@ export class HostService { }); } - const sortedCategories: any = Object.values(grouped) - .sort((a: any, b: any) => a.displayOrder - b.displayOrder); + const sortedCategories: any = Object.values(grouped).sort( + (a: any, b: any) => a.displayOrder - b.displayOrder, + ); for (const cat of sortedCategories) { - cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder); + cat.pqqsubCategories.sort( + (a: any, b: any) => a.displayOrder - b.displayOrder, + ); for (const sub of cat.pqqsubCategories) { - sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder); + sub.questions.sort( + (a: any, b: any) => a.displayOrder - b.displayOrder, + ); } } return { activity_xid: created.id, - sortedCategories + sortedCategories, }; }); } @@ -1896,7 +1970,7 @@ export class HostService { * This method will create Activities + ActivityOtherDetails + ActivitiesMedia + * ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine + * ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments + - * ActivityAmenities + ActivityEligibility + * ActivityAmenities + ActivityEligibility */ async createOrUpdateActivity( userId: number, @@ -1981,10 +2055,7 @@ async createOrUpdateActivity( } if (v.isMinPeopleReqMandatory && !v.minPeopleRequired) { - throw new ApiError( - 400, - `venues[${idx}] min people requirement missing`, - ); + throw new ApiError(400, `venues[${idx}] min people requirement missing`); } if (!Array.isArray(v.prices) || !v.prices.length) { @@ -2038,7 +2109,7 @@ async createOrUpdateActivity( } /* -------------------------------- - * 3️⃣ STATUS DECISION (YOUR LOGIC) + * 3️⃣ STATUS DECISION * -------------------------------- */ let activityInternalStatus; let activityDisplayStatus; @@ -2051,43 +2122,27 @@ async createOrUpdateActivity( if (wasRejected) { if (isDraft) { - activityInternalStatus = - existingActivity.activityInternalStatus; - activityDisplayStatus = - existingActivity.activityDisplayStatus; - amInternalStatus = - existingActivity.amInternalStatus; - amDisplayStatus = - existingActivity.amDisplayStatus; + activityInternalStatus = existingActivity.activityInternalStatus; + activityDisplayStatus = existingActivity.activityDisplayStatus; + amInternalStatus = existingActivity.amInternalStatus; + amDisplayStatus = existingActivity.amDisplayStatus; } else { - activityInternalStatus = - ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; - activityDisplayStatus = - ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; - amInternalStatus = - ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; - amDisplayStatus = - ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; + activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; + activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; + amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; + amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; } } else { if (isDraft) { - activityInternalStatus = - ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT; - activityDisplayStatus = - ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT; - amInternalStatus = - ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_DRAFT; - amDisplayStatus = - ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_DRAFT; + activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT; + activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT; + amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_DRAFT; + amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_DRAFT; } else { - activityInternalStatus = - ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; - activityDisplayStatus = - ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; - amInternalStatus = - ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; - amDisplayStatus = - ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; + activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED; + activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW; + amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW; + amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW; } } @@ -2097,19 +2152,39 @@ async createOrUpdateActivity( const activity = await tx.activities.update({ where: { id: existingActivity.id }, data: { + activityTypeXid: payload.activityTypeXid ?? undefined, + frequenciesXid: payload.frequenciesXid ?? undefined, activityTitle: payload.activityTitle ?? undefined, activityDescription: payload.activityDescription ?? undefined, + + checkInLat: payload.checkInLat ?? undefined, + checkInLong: payload.checkInLong ?? undefined, + checkInAddress: payload.checkInAddress ?? undefined, + isCheckOutSame: toBool(payload.isCheckOutSame), + checkOutLat: payload.checkOutLat ?? undefined, + checkOutLong: payload.checkOutLong ?? undefined, + checkOutAddress: payload.checkOutAddress ?? undefined, + + energyLevelXid: payload.energyLevelXid ?? undefined, + activityDurationMins: payload.activityDurationMins ?? undefined, + currencyXid: payload.currencyXid ?? undefined, + sustainabilityScore: payload.sustainabilityScore ?? undefined, + safetyScore: payload.safetyScore ?? undefined, isInstantBooking: payload.isInstantBooking ?? undefined, foodAvailable: payload.foodAvailable, + foodIsChargeable: toBool(payload.foodIsChargeable), alcoholAvailable: payload.alcoholAvailable, trainerAvailable: payload.trainerAvailable, + trainerIsChargeable: toBool(payload.trainerIsChargeable), pickUpDropAvailable: payload.pickUpDropAvailable, + pickUpDropIsChargeable: toBool(payload.pickUpDropIsChargeable), inActivityAvailable: payload.inActivityAvailable, + inActivityIsChargeable: toBool(payload.inActivityIsChargeable), equipmentAvailable: payload.equipmentAvailable, + equipmentIsChargeable: toBool(payload.equipmentIsChargeable), cancellationAvailable: payload.cancellationAvailable, - isCheckOutSame: payload.isCheckOutSame, activityInternalStatus, activityDisplayStatus, @@ -2121,22 +2196,47 @@ async createOrUpdateActivity( const activityXid = activity.id; /* -------------------------------- - * 5️⃣ CLEAN OLD VENUES + * 5️⃣ CLEAN OLD ACTIVITY MEDIA + * -------------------------------- */ + await tx.activitiesMedia.deleteMany({ where: { activityXid } }); + + /* -------------------------------- + * 6️⃣ SAVE NEW ACTIVITY MEDIA + * -------------------------------- */ + if (Array.isArray(payload.media) && payload.media.length) { + await tx.activitiesMedia.createMany({ + data: payload.media.map((m, index) => ({ + activityXid, + mediaType: m.mediaType ?? 'unknown', + mediaFileName: m.mediaFileName, + displayOrder: index + 1, + })), + }); + } + + /* -------------------------------- + * 7️⃣ CLEAN OLD VENUES & RELATED DATA * -------------------------------- */ const oldVenueIds = ( await tx.activityVenues.findMany({ where: { activityXid }, select: { id: true }, }) - ).map(v => v.id); + ).map((v) => v.id); if (oldVenueIds.length) { + // Clean venue artifacts (media) + await tx.activityVenueArtifacts.deleteMany({ + where: { activityVenueXid: { in: oldVenueIds } }, + }); + + // Clean price taxes and prices const priceIds = ( await tx.activityPrices.findMany({ where: { activityVenueXid: { in: oldVenueIds } }, select: { id: true }, }) - ).map(p => p.id); + ).map((p) => p.id); if (priceIds.length) { await tx.activityPriceTaxes.deleteMany({ @@ -2147,32 +2247,48 @@ async createOrUpdateActivity( }); } + // Clean venues await tx.activityVenues.deleteMany({ where: { id: { in: oldVenueIds } }, }); } /* -------------------------------- - * 6️⃣ CREATE VENUES (MULTIPLE) + * 8️⃣ CREATE VENUES WITH MEDIA & PRICES * -------------------------------- */ for (const venue of payload.venues ?? []) { const venueRow = await tx.activityVenues.create({ data: { activityXid, venueName: venue.venueName, - venueCapacity: venue.venueCapacity ?? 0, - availableSeats: venue.availableSeats ?? 0, + venueCapacity: toNumber(venue.venueCapacity) ?? 0, + availableSeats: toNumber(venue.availableSeats) ?? 0, isMinPeopleReqMandatory: venue.isMinPeopleReqMandatory, - minPeopleRequired: venue.minPeopleRequired ?? null, + minPeopleRequired: toNumber(venue.minPeopleRequired) ?? null, minReqfullfilledBeforeMins: - venue.minReqfullfilledBeforeMins ?? null, + toNumber(venue.minReqfullfilledBeforeMins) ?? null, + venueDescription: venue.venueDescription ?? null, }, }); - for (const price of venue.prices) { + // Create venue media/artifacts + if (Array.isArray(venue.media) && venue.media.length) { + await tx.activityVenueArtifacts.createMany({ + data: venue.media.map((m) => ({ + activityVenueXid: venueRow.id, + mediaType: m.mediaType ?? 'image', + mediaFileName: m.mediaFileName, + })), + }); + } + + // Create venue prices with taxes + for (const price of venue.prices ?? []) { const sellPrice = Number(price.sellPrice); - const { basePrice, taxDetails } = - computeBasePriceAndTaxes(sellPrice, rootTaxes); + const { basePrice, taxDetails } = computeBasePriceAndTaxes( + sellPrice, + rootTaxes, + ); const priceRow = await tx.activityPrices.create({ data: { @@ -2180,8 +2296,7 @@ async createOrUpdateActivity( noOfSession: price.noOfSession ?? 1, isPackage: price.isPackage ?? false, sessionValidity: price.sessionValidity ?? 0, - sessionValidityFrequency: - price.sessionValidityFrequency ?? 'Days', + sessionValidityFrequency: price.sessionValidityFrequency ?? 'Days', basePrice, sellPrice, }, @@ -2189,7 +2304,7 @@ async createOrUpdateActivity( if (taxDetails.length) { await tx.activityPriceTaxes.createMany({ - data: taxDetails.map(t => ({ + data: taxDetails.map((t) => ({ activityPriceXid: priceRow.id, taxXid: t.taxXid, taxPer: t.taxPer, @@ -2201,15 +2316,80 @@ async createOrUpdateActivity( } /* -------------------------------- - * 7️⃣ TRAINER + * 9️⃣ CLEAN & CREATE EQUIPMENT WITH TAXES * -------------------------------- */ - if (payload.trainerAvailable) { - const { basePrice, taxDetails } = - computeBasePriceAndTaxes( - payload.trainerTotalAmount, + const oldEquipmentIds = ( + await tx.activityEquipments.findMany({ + where: { activityXid }, + select: { id: true }, + }) + ).map((e) => e.id); + + if (oldEquipmentIds.length) { + await tx.activityEquipmentTaxes.deleteMany({ + where: { activityEquipmentXid: { in: oldEquipmentIds } }, + }); + await tx.activityEquipments.deleteMany({ + where: { id: { in: oldEquipmentIds } }, + }); + } + + if (Array.isArray(payload.equipments) && payload.equipments.length) { + for (const eq of payload.equipments) { + const totalPrice = toNumber(eq.equipmentTotalPrice) ?? 0; + const { basePrice, taxDetails } = computeBasePriceAndTaxes( + totalPrice, rootTaxes, ); + const equipment = await tx.activityEquipments.create({ + data: { + activityXid, + equipmentName: eq.equipmentName, + isEquipmentChargeable: toBool(eq.isEquipmentChargeable), + equipmentBasePrice: basePrice, + equipmentTotalPrice: totalPrice, + }, + }); + + if (taxDetails.length) { + await tx.activityEquipmentTaxes.createMany({ + data: taxDetails.map((t) => ({ + activityEquipmentXid: equipment.id, + taxXid: t.taxXid, + taxPer: t.taxPer, + taxAmount: t.taxAmount, + })), + }); + } + } + } + + /* -------------------------------- + * 🔟 CLEAN & CREATE TRAINER WITH TAXES + * -------------------------------- */ + const oldTrainerIds = ( + await tx.activityTrainers.findMany({ + where: { activityXid }, + select: { id: true }, + }) + ).map((t) => t.id); + + if (oldTrainerIds.length) { + await tx.activityTrainerTaxes.deleteMany({ + where: { activityTrainerXid: { in: oldTrainerIds } }, + }); + await tx.activityTrainers.deleteMany({ + where: { id: { in: oldTrainerIds } }, + }); + } + + if (payload.trainerAvailable) { + const { basePrice, taxDetails } = computeBasePriceAndTaxes( + payload.trainerTotalAmount, + rootTaxes, + ); + const trainer = await tx.activityTrainers.create({ data: { activityXid, @@ -2218,20 +2398,251 @@ async createOrUpdateActivity( }, }); - for (const t of taxDetails) { - await tx.activityTrainerTaxes.create({ - data: { + if (taxDetails.length) { + await tx.activityTrainerTaxes.createMany({ + data: taxDetails.map((t) => ({ activityTrainerXid: trainer.id, taxXid: t.taxXid, taxPer: t.taxPer, taxAmount: t.taxAmount, - }, + })), }); } } /* -------------------------------- - * 8️⃣ ACTIVITY TRACK + * 1️⃣1️⃣ CLEAN & CREATE PICKUP/DROP TRANSPORTS WITH DETAILS & TAXES + * -------------------------------- */ + const oldTransportIds = ( + await tx.activityPickUpTransport.findMany({ + where: { activityXid }, + select: { id: true }, + }) + ).map((t) => t.id); + + if (oldTransportIds.length) { + // Get all pickup details for these transports + const oldPickupDetailIds = ( + await tx.activityPickUpDetails.findMany({ + where: { activityPickUpTransportXid: { in: oldTransportIds } }, + select: { id: true }, + }) + ).map((p) => p.id); + + if (oldPickupDetailIds.length) { + // Delete taxes first + await tx.activityPickUpTransportTaxes.deleteMany({ + where: { activityPickUpDetailsXid: { in: oldPickupDetailIds } }, + }); + // Delete pickup details + await tx.activityPickUpDetails.deleteMany({ + where: { id: { in: oldPickupDetailIds } }, + }); + } + + // Delete transports + await tx.activityPickUpTransport.deleteMany({ + where: { id: { in: oldTransportIds } }, + }); + } + + if ( + Array.isArray(payload.pickupTransports) && + payload.pickupTransports.length + ) { + for (const transport of payload.pickupTransports) { + // Create transport mode + const transportRow = await tx.activityPickUpTransport.create({ + data: { + activityXid, + transportModeXid: transport.transportModeXid, + isTransportModeChargeable: toBool( + transport.isTransportModeChargeable, + ), + }, + }); + + // Create pickup details for this transport + if ( + Array.isArray(transport.pickupDetails) && + transport.pickupDetails.length + ) { + for (const detail of transport.pickupDetails) { + const totalPrice = toNumber(detail.transportTotalPrice) ?? 0; + const { basePrice, taxDetails } = computeBasePriceAndTaxes( + totalPrice, + rootTaxes, + ); + + const pickupDetail = await tx.activityPickUpDetails.create({ + data: { + activityPickUpTransportXid: transportRow.id, + isPickUp: toBool(detail.isPickUp), + locationLat: toNumber(detail.locationLat), + locationLong: toNumber(detail.locationLong), + locationAddress: detail.locationAddress ?? null, + transportBasePrice: basePrice, + transportTotalPrice: totalPrice, + }, + }); + + if (taxDetails.length) { + await tx.activityPickUpTransportTaxes.createMany({ + data: taxDetails.map((t) => ({ + activityPickUpDetailsXid: pickupDetail.id, + taxXid: t.taxXid, + taxPer: t.taxPer, + taxAmount: t.taxAmount, + })), + }); + } + } + } + } + } + + /* -------------------------------- + * 1️⃣2️⃣ CLEAN & CREATE NAVIGATION MODES WITH TAXES + * -------------------------------- */ + const oldNavIds = ( + await tx.activityNavigationModes.findMany({ + where: { activityXid }, + select: { id: true }, + }) + ).map((n) => n.id); + + if (oldNavIds.length) { + await tx.activityNavigationModesTaxes.deleteMany({ + where: { activityNavigationModeXid: { in: oldNavIds } }, + }); + await tx.activityNavigationModes.deleteMany({ + where: { id: { in: oldNavIds } }, + }); + } + + if ( + Array.isArray(payload.navigationModes) && + payload.navigationModes.length + ) { + const totalPrice = toNumber(payload.navigationModeTotalPrice) ?? 0; + const { basePrice, taxDetails } = computeBasePriceAndTaxes( + totalPrice, + rootTaxes, + ); + + for (const modeId of payload.navigationModes) { + const navMode = await tx.activityNavigationModes.create({ + data: { + activityXid, + navigationModeXid: modeId, + isInActivityChargeable: toBool(payload.navigationModeIsChargeable), + navigationModesBasePrice: basePrice, + navigationModesTotalPrice: totalPrice, + }, + }); + + if (taxDetails.length) { + await tx.activityNavigationModesTaxes.createMany({ + data: taxDetails.map((t) => ({ + activityNavigationModeXid: navMode.id, + taxXid: t.taxXid, + taxPer: t.taxPer, + taxAmount: t.taxAmount, + })), + }); + } + } + } + + /* -------------------------------- + * 1️⃣3️⃣ CLEAN & CREATE AMENITIES + * -------------------------------- */ + await tx.activityAmenities.deleteMany({ where: { activityXid } }); + + if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) { + await tx.activityAmenities.createMany({ + data: payload.amenitiesIds.map((amenityId) => ({ + activityXid, + amenitiesXid: amenityId, + })), + }); + } + + /* -------------------------------- + * 1️⃣4️⃣ CLEAN & CREATE ELIGIBILITY + * -------------------------------- */ + await tx.activityEligibility.deleteMany({ where: { activityXid } }); + + if (payload.eligibility) { + await tx.activityEligibility.create({ + data: { + activityXid, + isAgeRestriction: toBool(payload.eligibility.isAgeRestriction), + ageRestrictionXid: toNumber(payload.eligibility.ageRestrictionXid), + isWeightRestriction: toBool(payload.eligibility.isWeightRestriction), + weightRestrictionName: payload.eligibility.weightRestrictionName ?? null, + weightEntered: toNumber(payload.eligibility.weightEntered), + weightIn: payload.eligibility.weightIn ?? null, + minWeight: toNumber(payload.eligibility.minWeight), + maxWeight: toNumber(payload.eligibility.maxWeight), + isHeightRestriction: toBool(payload.eligibility.isHeightRestriction), + heightRestrictionName: payload.eligibility.heightRestrictionName ?? null, + heightEntered: toNumber(payload.eligibility.heightEntered), + heightIn: payload.eligibility.heightIn ?? null, + minHeight: toNumber(payload.eligibility.minHeight), + maxHeight: toNumber(payload.eligibility.maxHeight), + }, + }); + } + + /* -------------------------------- + * 1️⃣5️⃣ CLEAN & CREATE OTHER DETAILS + * -------------------------------- */ + await tx.activityOtherDetails.deleteMany({ where: { activityXid } }); + + if (payload.otherDetails) { + await tx.activityOtherDetails.create({ + data: { + activityXid, + exclusiveNotes: payload.otherDetails.exclusiveNotes ?? null, + dosNotes: payload.otherDetails.dosNotes ?? null, + dontsNotes: payload.otherDetails.dontsNotes ?? null, + tipsNotes: payload.otherDetails.tipsNotes ?? null, + termsAndCondition: payload.otherDetails.termsAndCondition ?? null, + }, + }); + } + + /* -------------------------------- + * 1️⃣6️⃣ CLEAN & CREATE FOOD TYPES + * -------------------------------- */ + await tx.activityFoodTypes.deleteMany({ where: { activityXid } }); + + if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) { + await tx.activityFoodTypes.createMany({ + data: payload.foodTypeIds.map((foodTypeId) => ({ + activityXid, + foodTypeXid: foodTypeId, + })), + }); + } + + /* -------------------------------- + * 1️⃣7️⃣ CLEAN & CREATE CUISINES + * -------------------------------- */ + await tx.activityCuisine.deleteMany({ where: { activityXid } }); + + if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) { + await tx.activityCuisine.createMany({ + data: payload.cuisineIds.map((cuisineId) => ({ + activityXid, + foodCuisineXid: cuisineId, + })), + }); + } + + /* -------------------------------- + * 1️⃣8️⃣ ACTIVITY TRACK * -------------------------------- */ await tx.activityTrack.create({ data: { @@ -2245,19 +2656,15 @@ async createOrUpdateActivity( }); /* -------------------------------- - * 9️⃣ RESPONSE + * 1️⃣9️⃣ RESPONSE * -------------------------------- */ return { activityXid, activityRefNumber: activity.activityRefNumber, - status: isDraft - ? 'ACTIVITY_SAVED_AS_DRAFT' - : 'ACTIVITY_SUBMITTED', + status: isDraft ? 'ACTIVITY_SAVED_AS_DRAFT' : 'ACTIVITY_SUBMITTED', }; }); } - - async getAllPQUpdatedResponse(activityXid: number) { const pqqHeaderData = await this.prisma.activityPQQheader.findMany({ where: { @@ -2283,10 +2690,10 @@ async createOrUpdateActivity( select: { id: true, categoryName: true, - displayOrder: true - } - } - } + displayOrder: true, + }, + }, + }, }, // 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION @@ -2296,11 +2703,11 @@ async createOrUpdateActivity( id: true, answerName: true, answerPoints: true, - displayOrder: true + displayOrder: true, }, - orderBy: { displayOrder: "asc" } - } - } + orderBy: { displayOrder: 'asc' }, + }, + }, }, ActivityPQQSuggestions: { where: { isActive: true }, @@ -2308,19 +2715,19 @@ async createOrUpdateActivity( id: true, title: true, comments: true, - activityPqqHeaderXid: true - } + activityPqqHeaderXid: true, + }, }, ActivityPQQSupportings: { where: { isActive: true }, select: { id: true, mediaType: true, - mediaFileName: true - } + mediaFileName: true, + }, }, }, - orderBy: { id: "asc" } + orderBy: { id: 'asc' }, }); // ---------- GROUPING START ---------- @@ -2338,11 +2745,11 @@ async createOrUpdateActivity( id: cat.id, categoryName: cat.categoryName, displayOrder: cat.displayOrder, - activityPqqHeaderId: item.id, // ✅ Added to match AM response - pqqsubCategories: [] + activityPqqHeaderId: item.id, // ✅ Added to match AM response + pqqsubCategories: [], }; } else if (!grouped[cat.id].activityPqqHeaderId) { - grouped[cat.id].activityPqqHeaderId = item.id; // Ensures it's set if missing + grouped[cat.id].activityPqqHeaderId = item.id; // Ensures it's set if missing } const category = grouped[cat.id]; @@ -2354,7 +2761,7 @@ async createOrUpdateActivity( id: sub.id, subCategoryName: sub.subCategoryName, displayOrder: sub.displayOrder, - questions: [] + questions: [], }; category.pqqsubCategories.push(subCat); } @@ -2367,18 +2774,21 @@ async createOrUpdateActivity( pqqAnswerXid: item.pqqAnswerXid, comments: item.comments || null, displayOrder: q.displayOrder, - allAnswerOptions: q.PQQAnswers || [], // 🔥 All answers + allAnswerOptions: q.PQQAnswers || [], // 🔥 All answers suggestions: item.ActivityPQQSuggestions, supportings: item.ActivityPQQSupportings, }); } // ---------- SORTING ---------- - const sortedCategories: any = Object.values(grouped) - .sort((a: any, b: any) => a.displayOrder - b.displayOrder); + const sortedCategories: any = Object.values(grouped).sort( + (a: any, b: any) => a.displayOrder - b.displayOrder, + ); for (const cat of sortedCategories) { - cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder); + cat.pqqsubCategories.sort( + (a: any, b: any) => a.displayOrder - b.displayOrder, + ); for (const sub of cat.pqqsubCategories) { sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder); @@ -2393,8 +2803,8 @@ async createOrUpdateActivity( for (const doc of q.supportings) { if (doc.mediaFileName) { const filePath = doc.mediaFileName; - const key = filePath.startsWith("http") - ? filePath.split(".com/")[1] + const key = filePath.startsWith('http') + ? filePath.split('.com/')[1] : filePath; doc.presignedUrl = await getPresignedUrl(bucket, key); From be65a6c021a7bdf394b822212ca37bc9cd53e501 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Mon, 22 Dec 2025 13:33:10 +0530 Subject: [PATCH 24/24] Added seed for energy level --- prisma/seed.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 71eaf9f..a97f7e5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -223,6 +223,15 @@ async function main() { ], skipDuplicates: true, // prevents error if already seeded }); + // ✅ Energy Levels + await prisma.energyLevels.createMany({ + data: [ + { energyLevelName: 'Low', energyIcon: '📶', energyColor: 'Red' }, + { energyLevelName: 'Medium', energyIcon: '📶', energyColor: 'Yellow' }, + { energyLevelName: 'High', energyIcon: '📶', energyColor: 'Green' }, + ], + skipDuplicates: true, // prevents error if already seeded + }); // ✅ Company types data await prisma.companyTypes.upsert({ @@ -260,7 +269,7 @@ async function main() { update: {}, create: { companyTypeName: 'Non-Profit Organisation', displayOrder: 6 }, }); - + await prisma.companyTypes.upsert({ where: { companyTypeName: 'Public Limited' }, update: {},