From 53785bd5f21e23d8aef87a86e5d763b29be1da5f Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 18 Dec 2025 19:37:32 +0530 Subject: [PATCH] Add handler for creating full activity with related records --- .../OnBoarding/CreateNewActivity.ts | 66 +++++ src/modules/host/services/host.service.ts | 272 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts new file mode 100644 index 0000000..59e15a5 --- /dev/null +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -0,0 +1,66 @@ +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../../common/utils/helper/ApiError'; +import { HostService } from '../../../services/host.service'; + +const hostService = new HostService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context, + ): Promise => { + // Verify authentication token + const token = + event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError( + 401, + 'This is a protected route. Please provide a valid token.', + ); + } + + // Verify token and get user info + const userInfo = await verifyHostToken(token); + + let body: any = {}; + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (err) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + // Accept multiple possible keys from the frontend payload + const rawActivityType = body.activityTypeXid ?? body.activityType ?? body.activity_type_xid; + const rawFrequencies = body.frequenciesXid ?? body.frequencies ?? body.frequencies_xid; + + const activityTypeXid = rawActivityType !== undefined && rawActivityType !== null ? Number(rawActivityType) : undefined; + const frequenciesXid = rawFrequencies !== undefined && rawFrequencies !== null ? Number(rawFrequencies) : undefined; + + if (!activityTypeXid || isNaN(activityTypeXid)) { + throw new ApiError(400, 'activityTypeXid is required and must be a number'); + } + + // Create full activity and related records + const createdData = await hostService.createFullActivity(userInfo.id, body); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Activity created successfully', + data: createdData + }), + }; + }, +); diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index cf4449e..dc3c519 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -1867,6 +1867,278 @@ export class HostService { }); } + /** + * Create a full activity with related records based on payload from the onboarding form. + * This method will create Activities + ActivityOtherDetails + ActivitiesMedia + + * ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine + + * ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments + + * ActivityAmenities + ActivityEligibility and also seed PQQ headers. + */ + async createFullActivity(userId: number, payload: any) { + return await this.prisma.$transaction(async (tx) => { + const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true } }); + if (!host) throw new ApiError(404, 'Host not found for the user'); + + const activityTypeXid = payload.activityTypeXid ?? payload.activityType ?? payload.activity_type_xid; + if (!activityTypeXid) throw new ApiError(400, 'activityTypeXid is required'); + + const activityType = await tx.activityTypes.findUnique({ where: { id: Number(activityTypeXid) } }); + if (!activityType) throw new ApiError(404, 'Activity type not found'); + + const frequenciesXid = payload.frequenciesXid ?? payload.frequencies ?? payload.frequencies_xid; + if (frequenciesXid) { + const freq = await tx.frequencies.findUnique({ where: { id: Number(frequenciesXid) } }); + if (!freq) throw new ApiError(404, 'Frequency not found'); + } + + const referenceNumber = await generateActivityRefNumber(tx); + + const activityData: any = { + hostXid: host.id, + activityTypeXid: Number(activityTypeXid), + frequenciesXid: frequenciesXid ? Number(frequenciesXid) : null, + activityRefNumber: referenceNumber, + }; + + // Simple mappings + if (payload.activityTitle) activityData.activityTitle = String(payload.activityTitle).substring(0, 30); + if (payload.activityDescription) activityData.activityDescription = String(payload.activityDescription).substring(0, 80); + if (payload.checkInLat) activityData.checkInLat = Number(payload.checkInLat); + if (payload.checkInLong) activityData.checkInLong = Number(payload.checkInLong); + if (payload.checkInAddress) activityData.checkInAddress = String(payload.checkInAddress).substring(0,150); + if (payload.isCheckOutSame !== undefined) activityData.isCheckOutSame = Boolean(payload.isCheckOutSame); + if (payload.checkOutLat) activityData.checkOutLat = Number(payload.checkOutLat); + if (payload.checkOutLong) activityData.checkOutLong = Number(payload.checkOutLong); + if (payload.checkOutAddress) activityData.checkOutAddress = String(payload.checkOutAddress).substring(0,150); + if (payload.energyLevelXid) activityData.energyLevelXid = Number(payload.energyLevelXid); + + // duration: accept minutes or hours+minutes + if (payload.activityDurationMins) activityData.activityDurationMins = Number(payload.activityDurationMins); + else if (payload.durationHours || payload.durationMins) { + const hrs = Number(payload.durationHours || 0); + const mins = Number(payload.durationMins || 0); + activityData.activityDurationMins = hrs * 60 + mins; + } + + // Booleans + const boolFields = [ + 'foodAvailable','foodIsChargeable','alcoholAvailable','trainerAvailable','trainerIsChargeable', + 'pickUpDropAvailable','pickUpDropIsChargeable','inActivityAvailable','inActivityIsChargeable', + 'equipmentAvailable','equipmentIsChargeable','cancellationAvailable','isInstantBooking' + ]; + for (const k of boolFields) { + if (payload[k] !== undefined) { + activityData[k] = Boolean(payload[k]); + } + } + + if (payload.currencyXid) activityData.currencyXid = Number(payload.currencyXid); + if (payload.sustainabilityScore) activityData.sustainabilityScore = Number(payload.sustainabilityScore); + if (payload.safetyScore) activityData.safetyScore = Number(payload.safetyScore); + + // Create activity + const createdActivity = await tx.activities.create({ data: activityData }); + + // Other details + if (payload.exclusiveNotes || payload.dosNotes || payload.dontsNotes || payload.tipsNotes || payload.termsAndCondition) { + await tx.activityOtherDetails.create({ + data: { + activityXid: createdActivity.id, + exclusiveNotes: payload.exclusiveNotes ? String(payload.exclusiveNotes).substring(0,50) : null, + dosNotes: payload.dosNotes ? String(payload.dosNotes).substring(0,200) : null, + dontsNotes: payload.dontsNotes ? String(payload.dontsNotes).substring(0,200) : null, + tipsNotes: payload.tipsNotes ? String(payload.tipsNotes).substring(0,100) : null, + termsAndCondition: payload.termsAndCondition ? String(payload.termsAndCondition).substring(0,500) : null, + } + }); + } + + // Media + if (Array.isArray(payload.media) && payload.media.length) { + const mediaData = payload.media.map((m: any, idx: number) => ({ + activityXid: createdActivity.id, + mediaType: String(m.mediaType || 'image'), + mediaFileName: String(m.mediaFileName), + displayOrder: idx + 1, + })); + await tx.activitiesMedia.createMany({ data: mediaData }); + } + + // Venues + Prices + if (Array.isArray(payload.venues) && payload.venues.length) { + for (const venue of payload.venues) { + const v = await tx.activityVenues.create({ + data: { + activityXid: createdActivity.id, + venueName: String(venue.venueName).substring(0,50), + venueCapacity: venue.venueCapacity ? Number(venue.venueCapacity) : 0, + availableSeats: venue.availableSeats ? Number(venue.availableSeats) : 0, + isMinPeopleReqMandatory: Boolean(venue.isMinPeopleReqMandatory || false), + minPeopleRequired: venue.minPeopleRequired ? Number(venue.minPeopleRequired) : null, + minReqfullfilledBeforeMins: venue.minReqfullfilledBeforeMins ? Number(venue.minReqfullfilledBeforeMins) : null, + venueDescription: venue.venueDescription ? String(venue.venueDescription).substring(0,200) : null, + } + }); + + if (Array.isArray(venue.prices) && venue.prices.length) { + const pricesData = venue.prices.map((p: any) => ({ + activityVenueXid: v.id, + noOfSession: Number(p.noOfSession || 1), + isPackage: Boolean(p.isPackage || false), + sessionValidity: Number(p.sessionValidity || 0), + sessionValidityFrequency: String(p.sessionValidityFrequency || 'Days'), + basePrice: Number(p.basePrice || 0), + sellPrice: Number(p.sellPrice || 0), + })); + await tx.activityPrices.createMany({ data: pricesData }); + } + } + } + + // Food types + if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) { + const ft = payload.foodTypeIds.map((id: any) => ({ activityXid: createdActivity.id, foodTypeXid: Number(id) })); + await tx.activityFoodTypes.createMany({ data: ft }); + } + + // Cuisines + if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) { + const cs = payload.cuisineIds.map((id: any) => ({ activityXid: createdActivity.id, foodCuisineXid: Number(id) })); + await tx.activityCuisine.createMany({ data: cs }); + } + + // Pick up transport + details + if (Array.isArray(payload.pickupTransports) && payload.pickupTransports.length) { + for (const pt of payload.pickupTransports) { + const transport = await tx.activityPickUpTransport.create({ + data: { + activityXid: createdActivity.id, + transportModeXid: Number(pt.transportModeXid), + isTransportModeChargeable: Boolean(pt.isTransportModeChargeable || false), + } + }); + + if (Array.isArray(pt.pickupDetails) && pt.pickupDetails.length) { + const pd = pt.pickupDetails.map((d: any) => ({ + activityPickUpTransportXid: transport.id, + isPickUp: Boolean(d.isPickUp || false), + locationLat: d.locationLat ? Number(d.locationLat) : null, + locationLong: d.locationLong ? Number(d.locationLong) : null, + locationAddress: d.locationAddress ? String(d.locationAddress).substring(0,150) : null, + transportBasePrice: d.transportBasePrice ? Number(d.transportBasePrice) : 0, + transportTotalPrice: d.transportTotalPrice ? Number(d.transportTotalPrice) : 0, + })); + await tx.activityPickUpDetails.createMany({ data: pd }); + } + } + } + + // Navigation modes + if (Array.isArray(payload.navigationModes) && payload.navigationModes.length) { + const navs = payload.navigationModes.map((n: any) => ({ + activityXid: createdActivity.id, + navigationModeXid: Number(n.navigationModeXid), + isInActivityChargeable: Boolean(n.isInActivityChargeable || false), + navigationModesBasePrice: Number(n.basePrice || 0), + navigationModesTotalPrice: Number(n.totalPrice || 0), + })); + await tx.activityNavigationModes.createMany({ data: navs }); + } + + // Equipments + if (Array.isArray(payload.equipments) && payload.equipments.length) { + const eqs = payload.equipments.map((e: any) => ({ + activityXid: createdActivity.id, + equipmentName: String(e.equipmentName).substring(0,30), + isEquipmentChargeable: Boolean(e.isEquipmentChargeable || false), + equipmentBasePrice: Number(e.equipmentBasePrice || 0), + equipmentTotalPrice: Number(e.equipmentTotalPrice || 0), + })); + await tx.activityEquipments.createMany({ data: eqs }); + } + + // Amenities + if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) { + const ams = payload.amenitiesIds.map((id: any) => ({ activityXid: createdActivity.id, amenitiesXid: Number(id) })); + await tx.activityAmenities.createMany({ data: ams }); + } + + // Eligibility + if (payload.eligibility) { + const e = payload.eligibility; + await tx.activityEligibility.create({ + data: { + activityXid: createdActivity.id, + isAgeRestriction: Boolean(e.isAgeRestriction || false), + ageRestrictionXid: e.ageRestrictionXid ? Number(e.ageRestrictionXid) : null, + isWeightRestriction: Boolean(e.isWeightRestriction || false), + weightRestrictionName: e.weightRestrictionName ? String(e.weightRestrictionName).substring(0,30) : null, + weightEntered: e.weightEntered ? Number(e.weightEntered) : null, + weightIn: e.weightIn ? String(e.weightIn).substring(0,30) : null, + minWeight: e.minWeight ? Number(e.minWeight) : null, + maxWeight: e.maxWeight ? Number(e.maxWeight) : null, + isHeightRestriction: Boolean(e.isHeightRestriction || false), + heightRestrictionName: e.heightRestrictionName ? String(e.heightRestrictionName).substring(0,30) : null, + heightEntered: e.heightEntered ? Number(e.heightEntered) : null, + minHeight: e.minHeight ? Number(e.minHeight) : null, + maxHeight: e.maxHeight ? Number(e.maxHeight) : null, + } + }); + } + + // ----------------- PQQ seeding (copy of existing logic) ----------------- + const questions = await tx.pQQCategories.findMany({ + where: { isActive: true }, + select: { + id: true, + categoryName: true, + displayOrder: true, + pqqsubCategories: { + where: { isActive: true }, + select: { + id: true, + subCategoryName: true, + displayOrder: true, + questions: { + where: { isActive: true }, + select: { + id: true, + questionName: true, + maxPoints: true, + displayOrder: true, + }, + orderBy: { displayOrder: 'asc' }, + }, + }, + orderBy: { displayOrder: 'asc' }, + }, + }, + orderBy: { displayOrder: 'asc' }, + }); + + const allQuestions: number[] = []; + for (const cat of questions) { + for (const sub of cat.pqqsubCategories) { + for (const q of sub.questions) { + allQuestions.push(q.id); + } + } + } + + if (allQuestions.length) { + await tx.activityPQQheader.createMany({ + data: allQuestions.map((id) => ({ + activityXid: createdActivity.id, + pqqQuestionXid: id, + pqqAnswerXid: null, + })), + }); + } + + return { activity_xid: createdActivity.id }; + }); + } + async getAllPQUpdatedResponse(activityXid: number) { const pqqHeaderData = await this.prisma.activityPQQheader.findMany({