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