From 5a223f126f346fdac9e9a6b3da48e624a25cee6e Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Sun, 21 Dec 2025 17:28:08 +0530 Subject: [PATCH] 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) {