diff --git a/layers/prisma/nodejs/package-lock.json b/layers/prisma/nodejs/package-lock.json
index 389329f..861854b 100644
--- a/layers/prisma/nodejs/package-lock.json
+++ b/layers/prisma/nodejs/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
- "pg": "^8.13.0"
+ "pg": "^8.13.0",
+ "zod": "^4.1.12"
}
},
"node_modules/@prisma/adapter-pg": {
@@ -223,6 +224,15 @@
"engines": {
"node": ">=0.4"
}
+ },
+ "node_modules/zod": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
+ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/layers/prisma/nodejs/package.json b/layers/prisma/nodejs/package.json
index 47d0046..5cd95ff 100644
--- a/layers/prisma/nodejs/package.json
+++ b/layers/prisma/nodejs/package.json
@@ -1,10 +1,11 @@
{
"name": "prisma-layer",
"version": "1.0.0",
- "description": "Lambda layer for Prisma 7 with pg driver adapter",
+ "description": "Lambda layer for Prisma 7 with pg driver adapter and zod",
"dependencies": {
"@prisma/client": "^7.0.1",
"@prisma/adapter-pg": "^7.0.1",
- "pg": "^8.13.0"
+ "pg": "^8.13.0",
+ "zod": "^4.1.12"
}
}
diff --git a/package-lock.json b/package-lock.json
index 5aa6fd1..9af9724 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46,7 +46,8 @@
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
- "serverless": "4.17.0",
+ "serverless": "4.24.0",
+ "swagger-ui-express": "^5.0.0",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"yup": "^1.7.1",
@@ -14491,12 +14492,12 @@
}
},
"node_modules/serverless": {
- "version": "4.17.0",
- "resolved": "https://registry.npmjs.org/serverless/-/serverless-4.17.0.tgz",
- "integrity": "sha512-hoZmipwyN/h7y9HwkWGlJ0YT06RFq7WNOD7fFEiPfnSnnUMVTzeNHq2BRrUlpHhf5s9srCHDc2wx5I06acfq1Q==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/serverless/-/serverless-4.24.0.tgz",
+ "integrity": "sha512-bgxFQ6QyOGJC9IZjZIXo4m6bdWMl9I7HNZ4jrmwSpdePdsRd46igGRpSnhdYFOc71GNplhSOeoCibL94yCHfrg==",
"hasInstallScript": true,
"dependencies": {
- "axios": "^1.8.3",
+ "axios": "^1.12.1",
"axios-proxy-builder": "^0.1.2",
"rimraf": "^5.0.5",
"xml2js": "0.6.2"
diff --git a/package.json b/package.json
index 81e9c17..ec4caaf 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,8 @@
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
- "serverless": "4.17.0",
+ "serverless": "4.24.0",
+ "swagger-ui-express": "^5.0.0",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"yup": "^1.7.1",
diff --git a/prisma/prisma.ts b/prisma/prisma.ts
index 61913bb..966c5cd 100644
--- a/prisma/prisma.ts
+++ b/prisma/prisma.ts
@@ -1,8 +1,8 @@
// prisma.ts
-import { PrismaClient } from '@prisma/client';
+// Re-export from the main singleton for consistency
+import { prisma } from '../src/common/database/prisma.client';
-// The DATABASE_URL environment variable will be automatically used
-export const prisma = new PrismaClient();
+export { prisma };
process.on('SIGINT', async () => {
await prisma.$disconnect();
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 4db51d4..56b18b8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -67,6 +67,11 @@ model User {
userAddressDetails UserAddressDetails[]
userDocuments UserDocuments[]
activityTracks ActivityTrack[]
+ // 🔹 Activities created by this user
+ createdActivities Activities[] @relation("UserActivities")
+
+ // 🔹 Activities where this user is Account Manager
+ managedActivities Activities[] @relation("ActivityAccountManager")
@@map("users")
@@schema("usr")
@@ -394,12 +399,13 @@ model DocumentType {
}
model FoodCuisines {
- id Int @id @default(autoincrement())
- cuisineName String @unique @map("cuisine_name") @db.VarChar(30)
- isActive Boolean @default(true) @map("is_active")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
+ id Int @id @default(autoincrement())
+ cuisineName String @unique @map("cuisine_name") @db.VarChar(30)
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+ activityCuisines ActivityCuisine[]
@@map("food_cuisines")
@@schema("mst")
@@ -434,13 +440,14 @@ model Amenities {
}
model FoodTypes {
- id Int @id @default(autoincrement())
- foodTypeName String @unique @map("food_type_name") @db.VarChar(30)
- isActive Boolean @default(true) @map("is_active")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
- ActivityFoodDetails ActivityFoodDetails[]
+ id Int @id @default(autoincrement())
+ foodTypeName String @unique @map("food_type_name") @db.VarChar(30)
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+ ActivityFoodCost ActivityFoodCost[]
+ activityFoodTypes ActivityFoodTypes[]
@@map("food_types")
@@schema("mst")
@@ -614,6 +621,7 @@ model EnergyLevels {
id Int @id @default(autoincrement())
energyLevelName String @map("energy_level_name") @db.VarChar(30)
energyIcon String @map("energy_icon") @db.VarChar(400)
+ energyColor String @map("energy_color") @db.VarChar(20)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -844,38 +852,45 @@ model HostTrack {
// ACTIVITY MODELS
model Activities {
- id Int @id @default(autoincrement())
- hostXid Int @map("host_xid")
- host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
- activityTypeXid Int @map("activity_type_xid")
- activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict)
- frequenciesXid Int? @map("frequencies_xid")
- frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
- activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
- activityTitle String? @map("activity_title") @db.VarChar(30)
- activityDescription String? @map("activity_description") @db.VarChar(80)
- checkInLat Float? @map("check_in_lat")
- checkInLong Float? @map("check_in_long")
- checkInAddress String? @map("check_in_address") @db.VarChar(150)
- isCheckOutSame Boolean? @default(true) @map("is_check_out_same")
- checkOutLat Float? @map("check_out_lat")
- checkOutLong Float? @map("check_out_long")
- checkOutAddress String? @map("check_out_address") @db.VarChar(150)
- energyLevelXid Int? @map("energy_level_xid")
- energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict)
- activityDurationMins Int? @map("activity_duration_mins")
- foodAvailable Boolean? @default(false) @map("food_available")
- foodIsChargeable Boolean? @default(false) @map("food_is_chargeable")
- alcoholAvailable Boolean? @default(false) @map("alcohol_available")
- trainerAvailable Boolean? @default(false) @map("trainer_available")
- trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable")
- pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available")
- pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable")
- inActivityAvailable Boolean? @default(false) @map("in_activity_available")
- inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable")
- equipmentAvailable Boolean? @default(false) @map("equipment_available")
- equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable")
- cancellationAvailable Boolean? @default(false) @map("cancellation_available")
+ id Int @id @default(autoincrement())
+ hostXid Int @map("host_xid")
+ host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
+ activityTypeXid Int @map("activity_type_xid")
+ activityType ActivityTypes @relation(fields: [activityTypeXid], references: [id], onDelete: Restrict)
+ frequenciesXid Int? @map("frequencies_xid")
+ frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
+ activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
+ activityTitle String? @map("activity_title") @db.VarChar(30)
+ activityDescription String? @map("activity_description") @db.VarChar(255)
+ checkInLat Float? @map("check_in_lat")
+ checkInLong Float? @map("check_in_long")
+ checkInAddress String? @map("check_in_address") @db.VarChar(150)
+ isCheckOutSame Boolean? @default(true) @map("is_check_out_same")
+ checkOutLat Float? @map("check_out_lat")
+ checkOutLong Float? @map("check_out_long")
+ checkOutAddress String? @map("check_out_address") @db.VarChar(150)
+ energyLevelXid Int? @map("energy_level_xid")
+ energyLevel EnergyLevels? @relation(fields: [energyLevelXid], references: [id], onDelete: Restrict)
+ activityDurationMins Int? @map("activity_duration_mins")
+ foodAvailable Boolean? @default(false) @map("food_available")
+ foodIsChargeable Boolean? @default(false) @map("food_is_chargeable")
+ alcoholAvailable Boolean? @default(false) @map("alcohol_available")
+ trainerAvailable Boolean? @default(false) @map("trainer_available")
+ trainerIsChargeable Boolean? @default(false) @map("trainer_is_chargeable")
+ pickUpDropAvailable Boolean? @default(false) @map("pick_up_drop_available")
+ pickUpDropIsChargeable Boolean? @default(false) @map("pick_up_drop_is_chargeable")
+ inActivityAvailable Boolean? @default(false) @map("in_activity_available")
+ inActivityIsChargeable Boolean? @default(false) @map("in_activity_is_chargeable")
+ equipmentAvailable Boolean? @default(false) @map("equipment_available")
+ equipmentIsChargeable Boolean? @default(false) @map("equipment_is_chargeable")
+ cancellationAvailable Boolean? @default(false) @map("cancellation_available")
+ // 🔹 Creator / owner
+ userId Int?
+ user User? @relation("UserActivities", fields: [userId], references: [id])
+
+ // 🔹 Account Manager
+ accountManagerXid Int?
+ accountManager User? @relation("ActivityAccountManager", fields: [accountManagerXid], references: [id], onDelete: Restrict)
cancellationAllowedBeforeMins Int? @map("cancellation_allowed_before_mins")
currencyXid Int? @map("currency_xid")
currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict)
@@ -898,19 +913,19 @@ model Activities {
ActivityEligibility ActivityEligibility[]
ActivitySuggestions ActivitySuggestions[]
ActivityAmDetails ActivityAmDetails[]
- ActivityPrices ActivityPrices[]
- ActivityVenueArtifacts ActivityVenueArtifacts[]
ActivityPQQheader ActivityPQQheader[]
ActivityAllowedEntry ActivityAllowedEntry[]
- ActivityFoodDetails ActivityFoodDetails[]
+ ActivityFoodCost ActivityFoodCost[]
ActivityEquipments ActivityEquipments[]
ActivityNavigationModes ActivityNavigationModes[]
ActivityPickUpDetails ActivityPickUpDetails[]
ActivityAmenities ActivityAmenities[]
- ActivityEquipmentTaxes ActivityEquipmentTaxes[]
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
activityTracks ActivityTrack[]
+ activityFoodTypes ActivityFoodTypes[]
+ activityCuisines ActivityCuisine[]
+ activityPickUpTransports ActivityPickUpTransport[]
@@map("activities")
@@schema("act")
@@ -920,8 +935,7 @@ model ActivityOtherDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
- foodCuisines String? @map("food_cuisines") @db.VarChar(30)
- exclusiveNotes String? @map("exclusive_notes") @db.VarChar(50)
+ 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)
@@ -987,6 +1001,8 @@ model ActivityVenues {
deletedAt DateTime? @map("deleted_at")
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
+ ActivityPrices ActivityPrices[] // <-- Added opposite relation
+ ActivityVenueArtifacts ActivityVenueArtifacts[] // <-- Added opposite relation
@@map("activity_venues")
@@schema("act")
@@ -1036,10 +1052,14 @@ model ActivityEligibility {
weightRestrictionName String? @map("weight_restriction_name") @db.VarChar(30)
weightEntered Int? @map("weight_entered")
weightIn String? @map("weight_in") @db.VarChar(30)
+ minWeight Int? @map("min_weight")
+ maxWeight Int? @map("max_weight")
isHeightRestriction Boolean @default(false) @map("is_height_restriction")
heightRestrictionName String? @map("height_restriction_name") @db.VarChar(30)
heightEntered Int? @map("height_entered")
heightIn String? @map("height_in") @db.VarChar(30)
+ minHeight Int? @map("min_height")
+ maxHeight Int? @map("max_height")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1086,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")
@@ -1123,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")
@@ -1204,12 +1224,10 @@ model ActivityAllowedEntry {
@@schema("act")
}
-model ActivityFoodDetails {
+model ActivityFoodCost {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
- foodTypeXid Int @map("food_type_xid")
- foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict)
baseAmount Int @map("base_amount")
totalAmount Int @map("total_amount")
isActive Boolean @default(true) @map("is_active")
@@ -1217,23 +1235,55 @@ model ActivityFoodDetails {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityFoodTaxes ActivityFoodTaxes[]
+ foodTypes FoodTypes? @relation(fields: [foodTypesId], references: [id])
+ foodTypesId Int?
- @@map("activity_food_details")
+ @@map("activity_food_cost")
+ @@schema("act")
+}
+
+model ActivityFoodTypes {
+ id Int @id @default(autoincrement())
+ activityXid Int @map("activity_xid")
+ activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
+ foodTypeXid Int @map("food_type_xid")
+ foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict)
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("activity_food_types")
+ @@schema("act")
+}
+
+model ActivityCuisine {
+ id Int @id @default(autoincrement())
+ activityXid Int @map("activity_xid")
+ activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
+ foodCuisineXid Int @map("food_cuisine_xid")
+ foodCuisine FoodCuisines @relation(fields: [foodCuisineXid], references: [id], onDelete: Restrict)
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("activity_cuisine")
@@schema("act")
}
model ActivityFoodTaxes {
- id Int @id @default(autoincrement())
- activityFoodDetailsXid Int @map("activity_food_details_xid")
- activityFoodDetails ActivityFoodDetails @relation(fields: [activityFoodDetailsXid], references: [id], onDelete: Cascade)
- taxXid Int @map("tax_xid")
- taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
- taxPer Float @map("tax_per")
- taxAmount Int @map("tax_amount")
- isActive Boolean @default(true) @map("is_active")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
+ id Int @id @default(autoincrement())
+ activityFoodCostXid Int @map("activity_food_cost_xid")
+ activityFoodCost ActivityFoodCost @relation(fields: [activityFoodCostXid], references: [id], onDelete: Cascade)
+ taxXid Int @map("tax_xid")
+ taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
+ taxPer Float @map("tax_per")
+ taxAmount Int @map("tax_amount")
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
@@map("activity_food_taxes")
@@schema("act")
@@ -1251,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")
@@ -1259,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")
@@ -1310,54 +1361,57 @@ model ActivityNavigationModesTaxes {
}
model ActivityPickUpDetails {
- id Int @id @default(autoincrement())
- activityXid Int @map("activity_xid")
- activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
- isPickUp Boolean @default(false) @map("is_pick_up")
- locationLat Float? @map("location_lat")
- locationLong Float? @map("location_long")
- locationAddress String? @map("location_address") @db.VarChar(150)
- isActive Boolean @default(true) @map("is_active")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
- ActivityPickUpTransport ActivityPickUpTransport[]
-
- @@map("activity_pick_up_details")
- @@schema("act")
-}
-
-model ActivityPickUpTransport {
id Int @id @default(autoincrement())
- activityPickUpDetailsXid Int @map("activity_pick_up_details_xid")
- activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade)
- transportModeXid Int @map("transport_mode_xid")
- transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict)
- isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable")
+ activityPickUpTransportXid Int @map("activity_pick_up_transport_xid")
+ activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade)
+ isPickUp Boolean @default(false) @map("is_pick_up")
+ locationLat Float? @map("location_lat")
+ locationLong Float? @map("location_long")
+ locationAddress String? @map("location_address") @db.VarChar(150)
transportBasePrice Int @map("transport_base_price")
transportTotalPrice Int @map("transport_total_price")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
- ActivityPickUpTransportTaxes ActivityPickUpTransportTaxes[]
+ activities Activities? @relation(fields: [activitiesId], references: [id])
+ activitiesId Int?
+ activityPickUpTransportTaxes ActivityPickUpTransportTaxes[]
+
+ @@map("activity_pick_up_details")
+ @@schema("act")
+}
+
+model ActivityPickUpTransport {
+ id Int @id @default(autoincrement())
+ activityXid Int @map("activity_xid")
+ activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
+ transportModeXid Int @map("transport_mode_xid")
+ transportMode TransportModes @relation(fields: [transportModeXid], references: [id], onDelete: Restrict)
+ isTransportModeChargeable Boolean @default(false) @map("is_transport_mode_chargeable")
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ pickupDetails ActivityPickUpDetails[]
@@map("activity_pick_up_transport")
@@schema("act")
}
model ActivityPickUpTransportTaxes {
- id Int @id @default(autoincrement())
- activityPickUpTransportXid Int @map("activity_pick_up_transport_xid")
- activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade)
- taxXid Int @map("tax_xid")
- taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
- taxPer Float @map("tax_per")
- taxAmount Int @map("tax_amount")
- isActive Boolean @default(true) @map("is_active")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
+ id Int @id @default(autoincrement())
+ activityPickUpDetailsXid Int @map("activity_pick_up_details_xid")
+ activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade)
+ taxXid Int @map("tax_xid")
+ taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
+ taxPer Float @map("tax_per")
+ taxAmount Int @map("tax_amount")
+ isActive Boolean @default(true) @map("is_active")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
@@map("activity_pick_up_transport_taxes")
@@schema("act")
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 291cd0c..a97f7e5 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -223,6 +223,15 @@ async function main() {
],
skipDuplicates: true, // prevents error if already seeded
});
+ // ✅ Energy Levels
+ await prisma.energyLevels.createMany({
+ data: [
+ { energyLevelName: 'Low', energyIcon: '📶', energyColor: 'Red' },
+ { energyLevelName: 'Medium', energyIcon: '📶', energyColor: 'Yellow' },
+ { energyLevelName: 'High', energyIcon: '📶', energyColor: 'Green' },
+ ],
+ skipDuplicates: true, // prevents error if already seeded
+ });
// ✅ Company types data
await prisma.companyTypes.upsert({
@@ -250,9 +259,9 @@ async function main() {
});
await prisma.companyTypes.upsert({
- where: { companyTypeName: 'Private Limited, Public Limited' },
+ where: { companyTypeName: 'Private Limited' },
update: {},
- create: { companyTypeName: 'Private Limited, Public Limited', displayOrder: 5 },
+ create: { companyTypeName: 'Private Limited', displayOrder: 5 },
});
await prisma.companyTypes.upsert({
@@ -261,6 +270,12 @@ async function main() {
create: { companyTypeName: 'Non-Profit Organisation', displayOrder: 6 },
});
+ await prisma.companyTypes.upsert({
+ where: { companyTypeName: 'Public Limited' },
+ update: {},
+ create: { companyTypeName: 'Public Limited', displayOrder: 7 },
+ });
+
// ✅ Food Types
await prisma.foodTypes.createMany({
data: [
diff --git a/serverless.yml b/serverless.yml
index 060b40e..1a0918a 100644
--- a/serverless.yml
+++ b/serverless.yml
@@ -1,4 +1,4 @@
-service: minglarDev
+service: minglar
useDotenv: true
@@ -20,7 +20,9 @@ provider:
# Apply Prisma layer to all functions
# Reference the layer defined in this stack using CloudFormation Ref
layers:
- - !Ref PrismaLambdaLayer
+ # Use the exported stack output so deploy function works (expects a string ARN)
+ # For offline/local, fall back to an empty string so the CF lookup is optional.
+ - ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''}
apiGateway:
binaryMediaTypes:
- '*/*'
@@ -84,11 +86,12 @@ build:
platform: node
# Mark as external so they're not bundled into the JS
external:
- # - '@prisma/client'
- # - '.prisma/client'
- # - '.prisma'
+ - '@prisma/client'
+ - '.prisma/client'
+ - '.prisma'
- '@prisma/adapter-pg'
- 'pg'
+ - 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
@@ -99,10 +102,11 @@ build:
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- # - '@prisma/client'
- # - '.prisma'
- # - '.prisma/client'
+ - '@prisma/client'
+ - '.prisma'
+ - '.prisma/client'
- 'pg'
+ - 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
diff --git a/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/serverless/functions/minglaradmin.yml b/serverless/functions/minglaradmin.yml
index c660469..c718cd8 100644
--- a/serverless/functions/minglaradmin.yml
+++ b/serverless/functions/minglaradmin.yml
@@ -406,3 +406,20 @@ getAllPQPDetailsForAM:
- httpApi:
path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
method: get
+
+
+getSuggestionsForAM:
+ handler: src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.handler
+ memorySize: 384
+ package:
+ patterns:
+ - 'src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM**'
+ - 'src/modules/minglaradmin/services/**'
+ - ${file(./serverless/patterns/base.yml):pattern1}
+ - ${file(./serverless/patterns/base.yml):pattern2}
+ - ${file(./serverless/patterns/base.yml):pattern3}
+ - ${file(./serverless/patterns/base.yml):pattern4}
+ events:
+ - httpApi:
+ path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
+ method: get
diff --git a/serverless/functions/prepopulate.yml b/serverless/functions/prepopulate.yml
index 7521ddb..d9282b5 100644
--- a/serverless/functions/prepopulate.yml
+++ b/serverless/functions/prepopulate.yml
@@ -91,4 +91,19 @@ getFrequenciesOfActivity:
events:
- httpApi:
path: /prepopulate/get-all-Frequencies
- method: get
\ No newline at end of file
+ method: get
+
+getAddActivityPrePopulate:
+ handler: src/modules/prepopulate/handlers/getAddActivityPrePopulate.handler
+ memorySize: 384
+ package:
+ patterns:
+ - 'src/modules/prepopulate/**'
+ - ${file(./serverless/patterns/base.yml):pattern1}
+ - ${file(./serverless/patterns/base.yml):pattern2}
+ - ${file(./serverless/patterns/base.yml):pattern3}
+ - ${file(./serverless/patterns/base.yml):pattern4}
+ events:
+ - httpApi:
+ path: /prepopulate/get-add-activity-prepopulate
+ method: get
diff --git a/src/common/database/prisma.client.ts b/src/common/database/prisma.client.ts
index 1da7d2e..1831f66 100644
--- a/src/common/database/prisma.client.ts
+++ b/src/common/database/prisma.client.ts
@@ -1,11 +1,29 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
-const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
+// Singleton pattern for Prisma client - prevents "Too many database connections" error
+const globalForPrisma = globalThis as unknown as {
+ prisma: PrismaClient | undefined;
+};
-export const prisma = new PrismaClient({
- adapter,
- log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
-});
+function createPrismaClient() {
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
+
+ return new PrismaClient({
+ adapter,
+ log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
+ });
+}
+
+export const prisma = globalForPrisma.prisma ?? createPrismaClient();
+
+if (process.env.NODE_ENV !== 'production') {
+ globalForPrisma.prisma = prisma;
+}
+
+// For serverless environments, always cache the client
+if (process.env.IS_OFFLINE || process.env.AWS_LAMBDA_FUNCTION_NAME) {
+ globalForPrisma.prisma = prisma;
+}
diff --git a/src/common/database/prisma.lambda.service.ts b/src/common/database/prisma.lambda.service.ts
index 0aa5b78..e5409b6 100644
--- a/src/common/database/prisma.lambda.service.ts
+++ b/src/common/database/prisma.lambda.service.ts
@@ -1,22 +1,5 @@
-import { PrismaClient } from '@prisma/client';
-import { PrismaPg } from '@prisma/adapter-pg';
-
-const adapter = new PrismaPg({
- connectionString: process.env.DATABASE_URL!,
-});
-
-let prisma: PrismaClient;
-
-if (!(global as any).prisma) {
- (global as any).prisma = new PrismaClient({
- adapter,
- log:
- process.env.NODE_ENV === 'dev'
- ? ['query', 'info', 'warn', 'error']
- : ['error'],
- });
-}
-
-prisma = (global as any).prisma;
+// Re-export the singleton prisma client for Lambda handlers
+// This ensures all Lambda functions use the same cached connection
+import { prisma } from './prisma.client';
export const prismaClient = prisma;
\ No newline at end of file
diff --git a/src/common/database/prisma.service.ts b/src/common/database/prisma.service.ts
index bb6565f..b75245f 100644
--- a/src/common/database/prisma.service.ts
+++ b/src/common/database/prisma.service.ts
@@ -1,8 +1,15 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
+import { prisma } from './prisma.client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
+ constructor() {
+ super();
+ // Use the singleton instance
+ Object.assign(this, prisma);
+ }
+
async onModuleInit() {
await this.$connect();
}
diff --git a/src/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/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts
index 0012e4e..a6ca41a 100644
--- a/src/common/utils/validation/host/hostCompanyDetails.validation.ts
+++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts
@@ -2,8 +2,8 @@ import { z } from "zod";
export const parentCompanySchema = z.object({
companyName: z.string()
- .min(1, "Parent company name is required")
- .max(100, "Parent company name cannot exceed 100 characters"),
+ .max(100, "Parent company name cannot exceed 100 characters")
+ .optional(),
address1: z.string()
.max(150, "Address1 cannot exceed 150 characters")
@@ -44,7 +44,7 @@ export const parentCompanySchema = z.object({
}),
companyTypeXid: z.number()
- .min(1, "Company type XID is required"),
+ .optional(),
websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(),
diff --git a/src/modules/host/dto/createActivity.schema.ts b/src/modules/host/dto/createActivity.schema.ts
new file mode 100644
index 0000000..98d2874
--- /dev/null
+++ b/src/modules/host/dto/createActivity.schema.ts
@@ -0,0 +1,162 @@
+import { z } from 'zod';
+
+/* ================= MEDIA ================= */
+export const MediaDto = z.object({
+ mediaType: z.string().optional(), // "image/jpeg", "video/mp4", etc.
+ mediaFileName: z.string(), // S3 file URL
+});
+
+/* ================= PRICE =================
+ * ❌ No tax info here; 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(), // required
+});
+
+/* ================= VENUE ================= */
+export const VenueDto = z.object({
+ venueName: z.string(),
+ venueCapacity: z.number().int().optional().default(0),
+ availableSeats: z.number().int().optional().default(0),
+ isMinPeopleReqMandatory: z.boolean().optional().default(false),
+ minPeopleRequired: z.number().int().nullable().optional(),
+ minReqfullfilledBeforeMins: z.number().int().nullable().optional(),
+ venueDescription: z.string().optional(),
+
+ // ✅ new: media per venue (for ActivityVenueArtifacts)
+ media: z.array(MediaDto).optional().default([]),
+
+ // price list per venue
+ prices: z.array(PriceDto).optional().default([]),
+});
+
+/* ================= PICKUP / DROP ================= */
+export const PickupDetailDto = z.object({
+ isPickUp: z.boolean().optional().default(false),
+ 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),
+});
+
+export const PickupTransportDto = z.object({
+ transportModeXid: z.number().int(),
+ isTransportModeChargeable: z.boolean().optional().default(false),
+ pickupDetails: z.array(PickupDetailDto).optional().default([]),
+});
+
+/* ================= EQUIPMENT ================= */
+export const EquipmentDto = z.object({
+ equipmentName: z.string(),
+ isEquipmentChargeable: z.boolean().optional().default(false),
+ equipmentBasePrice: z.number().int().optional().default(0),
+ equipmentTotalPrice: z.number().int().optional().default(0),
+});
+
+/* ================= 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().nullable().optional(),
+ weightEntered: z.number().int().nullable().optional(),
+ 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().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(),
+});
+
+/* ================= OTHER DETAILS ================= */
+export const OtherDetailsDto = z.object({
+ exclusiveNotes: z.string().optional(),
+ dosNotes: z.string().optional(),
+ dontsNotes: z.string().optional(),
+ tipsNotes: z.string().optional(),
+ termsAndCondition: z.string().optional(),
+});
+
+/* ================= CREATE ACTIVITY ================= */
+export const CreateActivityDto = z.object({
+ /* 🔑 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(),
+
+ /* 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().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 / CURRENCY */
+ currencyXid: z.number().int().nullable().optional(),
+ 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([]),
+
+ /* 🔥 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([]),
+ 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(),
+});
+
+export type CreateActivityInput = z.infer;
diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts
new file mode 100644
index 0000000..cd77d0e
--- /dev/null
+++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts
@@ -0,0 +1,311 @@
+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 { 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',
+ '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): 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.',
+ );
+ }
+
+ const userInfo = await verifyHostToken(token);
+
+ /* 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');
+ }
+
+ /* 3️⃣ BODY BUFFER */
+ const bodyBuffer = event.isBase64Encoded
+ ? Buffer.from(event.body as string, 'base64')
+ : Buffer.from(event.body as string);
+
+ const fields: Record = {};
+ const files: Array<{
+ buffer: Buffer;
+ mimeType: string;
+ fileName: string;
+ fieldName: string;
+ }> = [];
+
+ 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',
+ 'activityTypeXid',
+ 'frequenciesXid',
+ 'trainerTotalAmount',
+ 'pickupDropTotalPrice',
+ 'navigationModeTotalPrice',
+ 'sustainabilityScore',
+ 'safetyScore',
+ 'checkInLat',
+ 'checkInLong',
+ 'checkOutLat',
+ 'checkOutLong',
+ ];
+
+ for (const key of numberKeys) {
+ if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') {
+ 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 ACTIVITY-LEVEL MEDIA (images/videos) */
+ const uploadedActivityMedia: Array<{ mediaType?: string; mediaFileName: string }> = [];
+
+ for (const file of files.filter(
+ (f) => f.fieldName === 'activityImages' || f.fieldName === 'activityVideos',
+ )) {
+ const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Media/${Date.now()}_${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();
+
+ uploadedActivityMedia.push({
+ mediaType: file.mimeType,
+ mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
+ });
+ }
+
+ /* 🔥 MERGE ACTIVITY MEDIA */
+ const existingMedia = Array.isArray(activityPayload.media)
+ ? activityPayload.media
+ : [];
+ activityPayload.media = [...existingMedia, ...uploadedActivityMedia];
+
+ /* 9️⃣ PROCESS VENUE MEDIA UPLOADS */
+ // Group venue files by index: venueImages[0], venueImages[1], etc.
+ const venueFilesMap: Map> = new Map();
+
+ 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) {
+ 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;
+ }
+
+ /* 1️⃣1️⃣ SAVE ACTIVITY */
+ const createdActivity = await hostService.createOrUpdateActivity(
+ userInfo.id,
+ parsedDto,
+ isDraft,
+ );
+
+ /* 1️⃣2️⃣ 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,
+ }),
+ };
+ },
+);
\ No newline at end of file
diff --git a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts
index de59512..c3b8493 100644
--- a/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts
+++ b/src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.ts
@@ -17,6 +17,16 @@ import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHo
const hostService = new HostService(prismaClient);
+function getExtensionFromMime(mimeType: string) {
+ const map: Record = {
+ 'image/jpeg': 'jpg',
+ 'image/png': 'png',
+ 'application/pdf': 'pdf',
+ 'image/webp': 'webp',
+ };
+ return map[mimeType] || 'bin';
+}
+
const s3 = new AWS.S3({
region: config.aws.region,
});
@@ -149,6 +159,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
}
+ if (
+ companyDetailsRaw.parentCompany &&
+ Object.values(companyDetailsRaw.parentCompany).every(
+ (v) => v === undefined || v === null
+ )
+ ) {
+ companyDetailsRaw.parentCompany = null;
+ }
+
/** 6) Profile update if provided */
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
@@ -284,11 +303,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
}
-
-
/** 11) UPLOAD DOCUMENTS */
async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) {
- const ext = originalName.split('.').pop() || 'jpg';
+ // const ext = originalName.split('.').pop() || 'jpg';
+ const ext = getExtensionFromMime(mimeType);
let s3Key = '';
@@ -362,31 +380,67 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
/** UPLOAD LOGO (if provided) */
- const logoFile = files.find((f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile');
- if (logoFile) {
- const logoUrl = await uploadToS3(logoFile.buffer, logoFile.mimeType, logoFile.fileName, 'logo');
+ const logoFile = files.find(
+ (f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
+ );
+
+ if (logoFile && logoFile.buffer && logoFile.fileName) {
+ const logoUrl = await uploadToS3(
+ logoFile.buffer,
+ logoFile.mimeType,
+ logoFile.fileName,
+ 'logo'
+ );
parsedCompany.logoPath = logoUrl;
}
/** UPLOAD PARENT COMPANY LOGO (if provided) */
- const parentLogoFile = files.find((f) => f.fieldName === 'parentCompanyLogo');
- if (parentLogoFile) {
+ const parentLogoFile = files.find(
+ (f) => f.fieldName === 'parentCompanyLogo'
+ );
+
+ if (parentLogoFile && parentLogoFile.buffer && parentLogoFile.mimeType) {
+ // 🔒 Only upload when an actual file is present
const parentLogoUrl = await uploadToS3(
parentLogoFile.buffer,
parentLogoFile.mimeType,
- parentLogoFile.fileName,
+ parentLogoFile.fileName, // safe here because it's a real file
'parent_company_logo',
);
if (parsedParentCompany) {
parsedParentCompany.logoPath = parentLogoUrl;
} else {
- // if no parent object exists yet (drafts or other flows), attach it safely
- parsedParentCompany = parsedParentCompany || {};
- parsedParentCompany.logoPath = parentLogoUrl;
+ parsedParentCompany = {
+ logoPath: parentLogoUrl,
+ };
}
}
+ if (parsedCompany.cityXid) {
+ const city = await prismaClient.cities.findUnique({
+ where: { id: Number(parsedCompany.cityXid) }
+ });
+ if (!city) {
+ throw new ApiError(400, `City with ID ${parsedCompany.cityXid} not found`);
+ }
+ }
+
+ if (!parsedCompany.isSubsidairy) {
+ const parentDocuments = await hostService.getParentDocumentsByHostId(userInfo.id);
+ if (parentDocuments.length > 0) {
+ for (const doc of parentDocuments) {
+ try {
+ const s3Key = getS3KeyFromUrl(doc.filePath);
+ await deleteFromS3(s3Key);
+ } catch (e) {
+ console.error("S3 delete failed:", doc.filePath, e);
+ }
+ }
+ }
+ await hostService.deleteExistingParentRecords(userInfo.id)
+ }
+
/** 12) SAVE / UPDATE HOST ENTRY */
const createdOrUpdated = await hostService.addOrUpdateCompanyDetails(
userInfo.id,
diff --git a/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts b/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts
index edfabfe..0853bd7 100644
--- a/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts
+++ b/src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.ts
@@ -43,7 +43,7 @@ export const handler = safeHandler(async (
// ✅ Validate payload using Zod
const validationResult = hostBankDetailsSchema.safeParse({
...(body as object),
- hostXid: host.id, // inject hostId from token (not from user input)
+ hostXid: host.host.id, // inject hostId from token (not from user input)
});
if (!validationResult.success) {
diff --git a/src/modules/host/handlers/getStepper.ts b/src/modules/host/handlers/getStepper.ts
index 38e3b63..8f9bc19 100644
--- a/src/modules/host/handlers/getStepper.ts
+++ b/src/modules/host/handlers/getStepper.ts
@@ -27,10 +27,6 @@ export const handler = safeHandler(async (
// Fetch user with their HostHeader stepper info
const host = await hostService.getHostIdByUserXid(userId);
- if (!host) {
- throw new ApiError(404, 'Host record not found');
- }
-
return {
statusCode: 200,
headers: {
@@ -41,7 +37,8 @@ export const handler = safeHandler(async (
success: true,
message: 'Stepper information retrieved successfully',
data: {
- stepper: host.stepper,
+ stepper: host?.host?.stepper || null,
+ emailAddress: host.user?.emailAddress || null,
},
}),
};
diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts
index d7f269e..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';
@@ -32,6 +34,14 @@ import {
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
import config from '../../../config/config';
+function sanitizeDocumentName(name?: string) {
+ if (!name) return null;
+
+ return name
+ .replace(/[^a-zA-Z0-9 _-]/g, '') // remove / .
+ .substring(0, 100);
+}
+
type HostCompanyDetailsInput = z.infer;
// Document input after S3 upload (with S3 URL as filePath)
@@ -52,18 +62,50 @@ 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) {
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()
export class HostService {
- constructor(private prisma: PrismaClient) { }
+ constructor(private prisma: PrismaClient) {}
async createHost(data: CreateHostDto) {
return this.prisma.user.create({ data });
@@ -76,9 +118,14 @@ export class HostService {
async getHostIdByUserXid(user_xid: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid },
- select: { id: true, companyName: true, countryXid: true, stepper: true },
+ select: { id: true, stepper: true },
});
- return host;
+
+ const user = await this.prisma.user.findUnique({
+ where: { id: user_xid },
+ select: { id: true, emailAddress: true },
+ });
+ return { host, user };
}
async getHostById(id: number) {
@@ -93,10 +140,10 @@ export class HostService {
filePath: true,
documentName: true,
documentTypeXid: true,
- documentType: true
- }
- }
- }
+ documentType: true,
+ },
+ },
+ },
},
HostBankDetails: true,
HostDocuments: {
@@ -113,7 +160,7 @@ export class HostService {
mobileNumber: true,
profileImage: true,
userRefNumber: true,
- }
+ },
},
user: {
select: {
@@ -125,7 +172,7 @@ export class HostService {
profileImage: true,
userStatus: true,
userRefNumber: true,
- }
+ },
},
companyTypes: {
select: {
@@ -144,7 +191,7 @@ export class HostService {
title: true,
comments: true,
isparent: true,
- }
+ },
},
countries: true,
currencies: true,
@@ -160,7 +207,6 @@ export class HostService {
}
if (host.HostDocuments?.length) {
-
for (const doc of host.HostDocuments) {
if (doc.filePath) {
const filePath = doc.filePath;
@@ -175,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);
@@ -203,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);
@@ -214,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);
@@ -290,21 +336,27 @@ export class HostService {
async loginForHost(emailAddress: string, userPassword: string) {
const existingUser = await this.prisma.user.findUnique({
- where: { emailAddress: emailAddress },
+ where: { emailAddress: emailAddress, isActive: true },
select: {
id: true,
roleXid: true,
+ firstName: true,
+ lastName: true,
+ emailAddress: true,
+ mobileNumber: true,
userPassword: true,
- userStatus: true
- }
+ userStatus: true,
+ },
});
- console.log(existingUser, "ajsbfkjd")
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) {
@@ -388,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({
@@ -403,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,
@@ -423,7 +479,7 @@ export class HostService {
data: [],
total: 0,
page: paginationOptions?.page || 1,
- limit: paginationOptions?.limit || 10
+ limit: paginationOptions?.limit || 10,
};
}
@@ -436,8 +492,8 @@ export class HostService {
{ activityTitle: { contains: term, mode: 'insensitive' } },
{
activityType: {
- activityTypeName: { contains: term, mode: 'insensitive' }
- }
+ activityTypeName: { contains: term, mode: 'insensitive' },
+ },
},
];
}
@@ -459,8 +515,8 @@ export class HostService {
frequency: {
select: {
id: true,
- frequencyName: true
- }
+ frequencyName: true,
+ },
},
ActivityAmDetails: {
select: {
@@ -483,10 +539,10 @@ export class HostService {
interests: {
select: {
id: true,
- interestName: true
- }
- }
- }
+ interestName: true,
+ },
+ },
+ },
},
},
skip: paginationOptions?.skip || 0,
@@ -501,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);
@@ -514,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 },
);
}
@@ -555,8 +613,8 @@ export class HostService {
id: true,
activityPqqHeaderXid: true,
mediaFileName: true,
- mediaType: true
- }
+ mediaType: true,
+ },
},
ActivityPQQSuggestions: {
where: { isActive: true, isReviewed: false },
@@ -564,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;
@@ -594,8 +651,8 @@ export class HostService {
activityXid: activity_xid,
isActive: true,
pqqAnswerXid: {
- not: null
- }
+ not: null,
+ },
},
select: {
pqqQuestionXid: true,
@@ -615,6 +672,50 @@ export class HostService {
});
}
+ async getParentDocumentsByHostId(userId: number) {
+ const host = await this.prisma.hostHeader.findFirst({
+ where: { userXid: userId },
+ select: { id: true },
+ });
+
+ if (!host) return [];
+
+ const parents = await this.prisma.hostParent.findMany({
+ where: { hostXid: host.id },
+ include: { HostParenetDocuments: true },
+ });
+
+ return parents.flatMap((p) => p.HostParenetDocuments);
+ }
+
+ async deleteExistingParentRecords(userId: number) {
+ const host = await this.prisma.hostHeader.findFirst({
+ where: { userXid: userId },
+ select: { id: true },
+ });
+
+ if (!host) return;
+
+ const parents = await this.prisma.hostParent.findMany({
+ where: { hostXid: host.id },
+ select: { id: true },
+ });
+
+ if (!parents.length) return;
+
+ const parentIds = parents.map((p) => p.id);
+
+ // 1️⃣ Delete documents first
+ await this.prisma.hostParenetDocuments.deleteMany({
+ where: { hostParentXid: { in: parentIds } },
+ });
+
+ // 2️⃣ Then delete parent records
+ await this.prisma.hostParent.deleteMany({
+ where: { id: { in: parentIds } },
+ });
+ }
+
async addOrUpdateCompanyDetails(
user_xid: number,
companyData: HostCompanyDetailsInput,
@@ -629,6 +730,7 @@ export class HostService {
where: { userXid: user_xid },
include: { hostParent: true },
});
+ console.log(existingHostCompany, '-: Existing hai');
let existingParentCompany;
@@ -637,9 +739,9 @@ export class HostService {
where: { hostXid: existingHostCompany.id },
select: {
id: true,
- logoPath: true
- }
- })
+ logoPath: true,
+ },
+ });
}
let hostStatusInternal;
@@ -657,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;
@@ -669,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
@@ -704,12 +808,17 @@ export class HostService {
// -------------------------------------------------------
if (!existingHostCompany) {
if (!isDraft) {
+ console.log('First time direct final submit.');
const existingByPan = await tx.hostHeader.findFirst({
where: { panNumber: companyData.panNumber },
});
if (existingByPan)
- throw new ApiError(400, 'Company already exists with this pan/bin number');
+ throw new ApiError(
+ 400,
+ 'Company already exists with this pan/bin number',
+ );
}
+ console.log('First Time Aaya hai');
const createdHost = await tx.hostHeader.create({
data: {
@@ -717,11 +826,17 @@ 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 || existingHostCompany.logoPath,
+ logoPath: companyData.logoPath || null,
isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber,
@@ -751,7 +866,7 @@ export class HostService {
const docsData = documents.map((doc) => ({
hostXid: createdHost.id,
documentTypeXid: doc.documentTypeXid,
- documentName: doc.documentName,
+ documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
}));
await tx.hostDocuments.createMany({ data: docsData });
@@ -759,23 +874,33 @@ export class HostService {
// parent create
if (companyData.isSubsidairy && parentCompanyData) {
+ console.log('Parent ke saath aaya hai first time.');
const createdParent = await tx.hostParent.create({
data: {
host: { connect: { id: createdHost.id } },
- companyName: parentCompanyData.companyName,
+ companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
- cities: parentCompanyData.cityXid
- ? { connect: { id: parentCompanyData.cityXid } }
- : undefined,
- states: parentCompanyData.stateXid
- ? { connect: { id: parentCompanyData.stateXid } }
- : undefined,
- countries: parentCompanyData.countryXid
- ? { connect: { id: parentCompanyData.countryXid } }
- : undefined,
+ // Safely handle city connection - only connect if valid ID exists
+ 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,
+
+ countries:
+ parentCompanyData?.countryXid &&
+ !isNaN(Number(parentCompanyData.countryXid))
+ ? { connect: { id: Number(parentCompanyData.countryXid) } }
+ : undefined,
pinCode: parentCompanyData.pinCode || null,
- logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath,
+ logoPath: parentCompanyData.logoPath || null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null,
@@ -798,7 +923,7 @@ export class HostService {
const parentDocsData = parentDocuments.map((doc) => ({
hostParentXid: createdParent.id,
documentTypeXid: doc.documentTypeXid,
- documentName: doc.documentName,
+ documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
}));
await tx.hostParenetDocuments.createMany({ data: parentDocsData });
@@ -827,9 +952,23 @@ export class HostService {
companyName: companyData.companyName,
address1: companyData.address1,
address2: companyData.address2,
- cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined,
- states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined,
- countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined,
+ // Safely handle city connection - only connect if valid ID exists
+ cities:
+ companyData.cityXid && !isNaN(Number(companyData.cityXid))
+ ? { connect: { id: Number(companyData.cityXid) } }
+ : undefined, // Don't change if not provided
+
+ // Same for state
+ states:
+ companyData.stateXid && !isNaN(Number(companyData.stateXid))
+ ? { connect: { id: Number(companyData.stateXid) } }
+ : undefined,
+
+ // Same for country
+ countries:
+ companyData.countryXid && !isNaN(Number(companyData.countryXid))
+ ? { connect: { id: Number(companyData.countryXid) } }
+ : undefined,
pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath,
isSubsidairy: companyData.isSubsidairy,
@@ -859,6 +998,7 @@ export class HostService {
// documents UPSERT
if (documents?.length) {
for (const doc of documents) {
+ if (!doc.filePath) continue;
const existingDoc = await tx.hostDocuments.findFirst({
where: {
hostXid: updatedHost.id,
@@ -871,7 +1011,9 @@ export class HostService {
where: { id: existingDoc.id },
data: {
filePath: doc.filePath,
- documentName: doc.documentName || existingDoc.documentName,
+ documentName:
+ sanitizeDocumentName(doc.documentName) ||
+ existingDoc.documentName,
},
});
} else {
@@ -879,7 +1021,7 @@ export class HostService {
data: {
hostXid: updatedHost.id,
documentTypeXid: doc.documentTypeXid,
- documentName: doc.documentName,
+ documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
@@ -890,26 +1032,40 @@ export class HostService {
// parent logic untouched
if (companyData.isSubsidairy) {
const parentRecords = existingHostCompany.hostParent;
- const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords;
-
+ const parentRecord = Array.isArray(parentRecords)
+ ? parentRecords[0]
+ : parentRecords;
+ console.log('Yaha aaya update in the apretn me');
if (!parentRecord) {
+ console.log('Parent record nahi mila to create kar raha hai.');
const createdParent = await tx.hostParent.create({
data: {
host: { connect: { id: updatedHost.id } },
- companyName: parentCompanyData.companyName,
+ companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
- cities: parentCompanyData.cityXid
- ? { connect: { id: parentCompanyData.cityXid } }
- : undefined,
- states: parentCompanyData.stateXid
- ? { connect: { id: parentCompanyData.stateXid } }
- : undefined,
- countries: parentCompanyData.countryXid
- ? { connect: { id: parentCompanyData.countryXid } }
- : 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,
+
+ countries:
+ parentCompanyData?.countryXid &&
+ !isNaN(Number(parentCompanyData.countryXid))
+ ? { connect: { id: Number(parentCompanyData.countryXid) } }
+ : undefined,
pinCode: parentCompanyData.pinCode || null,
- logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath,
+ logoPath:
+ parentCompanyData?.logoPath ||
+ existingParentCompany?.logoPath ||
+ null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null,
@@ -933,7 +1089,7 @@ export class HostService {
data: {
hostParentXid: createdParent.id,
documentTypeXid: doc.documentTypeXid,
- documentName: doc.documentName,
+ documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
@@ -943,20 +1099,31 @@ export class HostService {
await tx.hostParent.update({
where: { id: parentRecord.id },
data: {
- companyName: parentCompanyData.companyName,
+ companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
- cities: parentCompanyData.cityXid
- ? { connect: { id: parentCompanyData.cityXid } }
- : undefined,
- states: parentCompanyData.stateXid
- ? { connect: { id: parentCompanyData.stateXid } }
- : undefined,
- countries: parentCompanyData.countryXid
- ? { connect: { id: parentCompanyData.countryXid } }
- : 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,
+
+ countries:
+ parentCompanyData?.countryXid &&
+ !isNaN(Number(parentCompanyData.countryXid))
+ ? { connect: { id: Number(parentCompanyData.countryXid) } }
+ : undefined,
pinCode: parentCompanyData.pinCode || null,
- logoPath: parentCompanyData.logoPath || existingParentCompany.logoPath,
+ logoPath:
+ parentCompanyData?.logoPath ||
+ existingParentCompany?.logoPath ||
+ null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null,
@@ -976,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: doc.documentName || existingParentDoc.documentName,
+ documentName:
+ sanitizeDocumentName(doc.documentName) ||
+ existingParentDoc.documentName,
},
});
} else {
@@ -996,7 +1167,7 @@ export class HostService {
data: {
hostParentXid: parentRecord.id,
documentTypeXid: doc.documentTypeXid,
- documentName: doc.documentName,
+ documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
@@ -1005,12 +1176,17 @@ export class HostService {
}
}
} else {
+ 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;
}
@@ -1048,8 +1224,6 @@ export class HostService {
});
}
-
-
async getSuggestionDetails(user_xid: number) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid, isActive: true },
@@ -1059,7 +1233,7 @@ export class HostService {
id: true,
emailAddress: true,
firstName: true,
- userRefNumber: true
+ userRefNumber: true,
},
},
accountManager: {
@@ -1219,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);
@@ -1239,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 {
@@ -1272,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,
@@ -1284,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,
@@ -1329,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 },
@@ -1345,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: {
@@ -1356,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({
@@ -1368,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: {
@@ -1379,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(
@@ -1413,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) {
@@ -1453,11 +1635,11 @@ export class HostService {
id: true,
categoryName: true,
displayOrder: true,
- }
- }
- }
- }
- }
+ },
+ },
+ },
+ },
+ },
},
ActivityPQQSuggestions: {
select: {
@@ -1467,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,
- }
+ },
},
},
});
@@ -1525,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 },
@@ -1572,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 },
});
@@ -1678,10 +1857,10 @@ export class HostService {
select: {
id: true,
categoryName: true,
- displayOrder: true
- }
- }
- }
+ displayOrder: true,
+ },
+ },
+ },
},
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
@@ -1691,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 },
@@ -1703,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 ------------------
@@ -1732,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);
}
@@ -1761,25 +1942,729 @@ 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,
};
});
}
+ /**
+ * 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
+ */
+async createOrUpdateActivity(
+ userId: number,
+ payload: any,
+ isDraft: boolean,
+) {
+ /* =====================================================
+ * HELPERS
+ * ===================================================== */
+ const toBool = (v: any) =>
+ v === true || v === 'true' || v === 1 || v === '1';
+ const toNumber = (v: any) =>
+ v === undefined || v === null || v === '' ? undefined : Number(v);
+
+ const round2 = (v: number) => Math.round(v);
+
+ const computeBasePriceAndTaxes = (
+ sellPrice: number,
+ taxes: Array<{ id: number; taxPer: number }>,
+ ) => {
+ if (!taxes.length) {
+ return { basePrice: round2(sellPrice), taxDetails: [] };
+ }
+
+ const totalTaxPer = taxes.reduce((s, t) => s + Number(t.taxPer || 0), 0);
+ const basePrice = round2(sellPrice / (1 + totalTaxPer / 100));
+
+ 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,
+ isActive: true,
+ },
+ });
+ if (!existingActivity) {
+ throw new ApiError(404, 'Activity not found');
+ }
+
+ /* --------------------------------
+ * 3️⃣ STATUS DECISION
+ * -------------------------------- */
+ let activityInternalStatus;
+ let activityDisplayStatus;
+ let amInternalStatus;
+ let amDisplayStatus;
+
+ 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;
+ }
+ }
+
+ /* --------------------------------
+ * 4️⃣ UPDATE ACTIVITY CORE + FLAGS
+ * -------------------------------- */
+ 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,
+
+ activityInternalStatus,
+ activityDisplayStatus,
+ amInternalStatus,
+ amDisplayStatus,
+ },
+ });
+
+ const activityXid = activity.id;
+
+ /* --------------------------------
+ * 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);
+
+ 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);
+
+ if (priceIds.length) {
+ await tx.activityPriceTaxes.deleteMany({
+ where: { activityPriceXid: { in: priceIds } },
+ });
+ await tx.activityPrices.deleteMany({
+ where: { id: { in: priceIds } },
+ });
+ }
+
+ // Clean venues
+ await tx.activityVenues.deleteMany({
+ where: { id: { in: oldVenueIds } },
+ });
+ }
+
+ /* --------------------------------
+ * 8️⃣ CREATE VENUES WITH MEDIA & PRICES
+ * -------------------------------- */
+ for (const venue of payload.venues ?? []) {
+ const venueRow = await tx.activityVenues.create({
+ data: {
+ activityXid,
+ venueName: venue.venueName,
+ venueCapacity: toNumber(venue.venueCapacity) ?? 0,
+ availableSeats: toNumber(venue.availableSeats) ?? 0,
+ isMinPeopleReqMandatory: venue.isMinPeopleReqMandatory,
+ minPeopleRequired: toNumber(venue.minPeopleRequired) ?? null,
+ minReqfullfilledBeforeMins:
+ toNumber(venue.minReqfullfilledBeforeMins) ?? null,
+ venueDescription: venue.venueDescription ?? null,
+ },
+ });
+
+ // 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 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,
+ })),
+ });
+ }
+ }
+ }
+
+ /* --------------------------------
+ * 9️⃣ CLEAN & CREATE EQUIPMENT WITH TAXES
+ * -------------------------------- */
+ 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,
+ baseAmount: basePrice,
+ totalAmount: payload.trainerTotalAmount,
+ },
+ });
+
+ if (taxDetails.length) {
+ await tx.activityTrainerTaxes.createMany({
+ data: taxDetails.map((t) => ({
+ activityTrainerXid: trainer.id,
+ taxXid: t.taxXid,
+ taxPer: t.taxPer,
+ taxAmount: t.taxAmount,
+ })),
+ });
+ }
+ }
+
+ /* --------------------------------
+ * 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: {
+ activityXid,
+ trackType: 'ACTIVITY',
+ trackStatus: activityInternalStatus,
+ updatedByXid: userId,
+ updatedByRole: ROLE_NAME.HOST,
+ updatedOn: new Date(),
+ },
+ });
+
+ /* --------------------------------
+ * 1️⃣9️⃣ RESPONSE
+ * -------------------------------- */
+ return {
+ activityXid,
+ activityRefNumber: activity.activityRefNumber,
+ status: isDraft ? 'ACTIVITY_SAVED_AS_DRAFT' : 'ACTIVITY_SUBMITTED',
+ };
+ });
+}
async getAllPQUpdatedResponse(activityXid: number) {
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
where: {
@@ -1805,10 +2690,10 @@ export class HostService {
select: {
id: true,
categoryName: true,
- displayOrder: true
- }
- }
- }
+ displayOrder: true,
+ },
+ },
+ },
},
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
@@ -1818,11 +2703,11 @@ export class HostService {
id: true,
answerName: true,
answerPoints: true,
- displayOrder: true
+ displayOrder: true,
},
- orderBy: { displayOrder: "asc" }
- }
- }
+ orderBy: { displayOrder: 'asc' },
+ },
+ },
},
ActivityPQQSuggestions: {
where: { isActive: true },
@@ -1830,19 +2715,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 START ----------
@@ -1860,11 +2745,11 @@ export class HostService {
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];
@@ -1876,7 +2761,7 @@ export class HostService {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
- questions: []
+ questions: [],
};
category.pqqsubCategories.push(subCat);
}
@@ -1889,18 +2774,21 @@ export class HostService {
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);
@@ -1915,8 +2803,8 @@ export class HostService {
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);
diff --git a/src/modules/host/services/resendOTPEmail.service.ts b/src/modules/host/services/resendOTPEmail.service.ts
index f7a2a34..1561e7f 100644
--- a/src/modules/host/services/resendOTPEmail.service.ts
+++ b/src/modules/host/services/resendOTPEmail.service.ts
@@ -15,8 +15,8 @@ export async function resendOtpEmail(
const htmlContent = `
Dear ${role},
Your new OTP is: ${otp}
- This code is valid for 5 minutes. Please do not share it with anyone.
- Best regards,
Minglar Team
+ This code will be valid for the next 5 minutes.
+ Warm regards,
Minglar Team
`;
try {
diff --git a/src/modules/host/services/sendOTPEmail.service.ts b/src/modules/host/services/sendOTPEmail.service.ts
index 15c1e70..e1d9b1f 100644
--- a/src/modules/host/services/sendOTPEmail.service.ts
+++ b/src/modules/host/services/sendOTPEmail.service.ts
@@ -13,9 +13,10 @@ export async function sendOtpEmailForHost(
const htmlContent = `
Dear Host,
- Your OTP for registration is: ${otp}
- This code is valid for 5 minutes. Please do not share it with anyone.
- Best regards,
Minglar Team
+ You’re almost all set! 🎉
+ Enter ${otp} to wrap your registration.
+ This code will be valid for the next 5 minutes.
+ Warm regards,
Minglar Team
`;
try {
diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts
index 803d32d..f3fef25 100644
--- a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts
+++ b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.ts
@@ -47,7 +47,7 @@ export const handler = safeHandler(async (
// Add suggestion using service
await minglarService.acceptHostApplication(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(hostXid)
- await sendEmailToHostForApprovedApplication(hostDetails.emailAddress)
+ await sendEmailToHostForApprovedApplication(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 200,
diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts
index f5ff82a..d447556 100644
--- a/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts
+++ b/src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.ts
@@ -4,6 +4,7 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
+import { sendAMPQQAcceptanceMailtoHost } from '../../../../minglaradmin/services/approvalMailtoHost.service';
const minglarService = new MinglarService(prismaClient);
@@ -39,6 +40,9 @@ export const handler = safeHandler(async (
Number(activityId),
Number(userInfo.id)
);
+ const hostXid = await minglarService.getHostXidByActivityId(activityId)
+ const hostDetails = await minglarService.getUserDetails(hostXid)
+ await sendAMPQQAcceptanceMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 201,
diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts
index 2539f75..4e47a59 100644
--- a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts
+++ b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.ts
@@ -47,7 +47,7 @@ export const handler = safeHandler(async (
// Add suggestion using service
await minglarService.rejectHostApplicationAM(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(hostXid)
- await sendAMRejectionMailtoHost(hostDetails.emailAddress)
+ await sendAMRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 200,
diff --git a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts
index 322c1f8..38e4886 100644
--- a/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts
+++ b/src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.ts
@@ -4,6 +4,7 @@ import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/a
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
+import { sendAMPQQRejectionMailtoHost } from '../../../../minglaradmin/services/rejectionMailtoHost.service';
const minglarService = new MinglarService(prismaClient);
@@ -39,6 +40,9 @@ export const handler = safeHandler(async (
Number(activityId),
Number(userInfo.id)
);
+ const hostXid = await minglarService.getHostXidByActivityId(activityId)
+ const hostDetails = await minglarService.getUserDetails(hostXid)
+ await sendAMPQQRejectionMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return {
statusCode: 201,
diff --git a/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts
new file mode 100644
index 0000000..dd83b3d
--- /dev/null
+++ b/src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.ts
@@ -0,0 +1,51 @@
+import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
+import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
+import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
+import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
+import ApiError from '../../../../../common/utils/helper/ApiError';
+import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
+
+const minglarService = new MinglarService(prismaClient);
+
+/**
+ * Get suggestions handler
+ * Retrieves suggestions based on user's role and host assignments
+ */
+export const handler = safeHandler(async (
+ event: APIGatewayProxyEvent,
+ context?: Context
+): Promise => {
+ // Verify authentication token
+ const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
+ if (!token) {
+ throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
+ }
+
+ // Verify token and get user info
+ await verifyMinglarAdminToken(token);
+
+ const hostXid = Number(event.pathParameters?.hostXid)
+
+ if (!hostXid) {
+ throw new ApiError(
+ 400,
+ 'Host ID is required in path parameters.',
+ );
+ }
+
+ // Get suggestions using service
+ const suggestions = await minglarService.getSuggestionsForAM(hostXid);
+
+ return {
+ statusCode: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ },
+ body: JSON.stringify({
+ success: true,
+ message: 'Suggestions retrieved successfully',
+ data: suggestions,
+ }),
+ };
+});
diff --git a/src/modules/minglaradmin/services/amNotification.service.ts b/src/modules/minglaradmin/services/amNotification.service.ts
index d38bf39..2990159 100644
--- a/src/modules/minglaradmin/services/amNotification.service.ts
+++ b/src/modules/minglaradmin/services/amNotification.service.ts
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
-import { PrismaService } from '../../../common/database/prisma.lambda.service';
+import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { sendAMEmailForHostAssign } from './AMEmail.service';
@Injectable()
export class AMNotificationService {
- constructor(private prisma: PrismaService) {}
+ private prisma = prismaClient;
/**
* Fetch account manager email by id and send assignment email.
diff --git a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts
index 30a8e24..06a169a 100644
--- a/src/modules/minglaradmin/services/approvalMailtoHost.service.ts
+++ b/src/modules/minglaradmin/services/approvalMailtoHost.service.ts
@@ -4,6 +4,7 @@ import config from "../../../config/config";
export async function sendEmailToHostForApprovedApplication(
emailAddress: string,
+ name: string
): Promise<{
sent: boolean;
// messageId: string
@@ -12,7 +13,7 @@ export async function sendEmailToHostForApprovedApplication(
const subject = "Approval for your application";
const htmlContent = `
- Dear Host,
+ Dear ${name},
Congratulations, Your application to minglar admin has been approved.
You can start onboarding your activities through the host panel.
You can login to your account using the link below:
@@ -73,3 +74,41 @@ export async function sendEmailToHostForMinglarApproval(
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
}
}
+
+export async function sendAMPQQAcceptanceMailtoHost(
+ emailAddress: string,
+ name: string
+): Promise<{
+ sent: boolean;
+ // messageId: string
+}> {
+
+ const subject = "Approval for your activity onboarding application";
+
+ const htmlContent = `
+
Dear ${name},
+ Congratulations, Your activity onboarding application to minglar admin has been approved.
+ You can start adding other details of your activity through the host panel.
+ You can login to your account using the link below:
+ Link: ${config.HOST_LINK}
+ Best regards,
Minglar Team
+ `;
+
+ try {
+ const result = await brevoService.sendEmail({
+ recipients: [{ email: emailAddress }],
+ subject,
+ htmlContent,
+ });
+
+ console.log("📧 Email sent successfully:", result);
+
+ return {
+ sent: true,
+ // messageId: result.messageId
+ };
+ } catch (err) {
+ console.error("Brevo email send failed:", err);
+ throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
+ }
+}
diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts
index 19f1153..6608294 100644
--- a/src/modules/minglaradmin/services/minglar.service.ts
+++ b/src/modules/minglaradmin/services/minglar.service.ts
@@ -136,6 +136,13 @@ export class MinglarService {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
+ async getHostXidByActivityId(activityId: number) {
+ const activityDetails = await this.prisma.activities.findFirst({
+ where: { id: activityId }
+ })
+ return activityDetails.hostXid;
+ }
+
async getUserDetails(id: number) {
const hostDetail = await this.prisma.hostHeader.findFirst({
where: { id: id }
@@ -741,21 +748,60 @@ export class MinglarService {
};
/** SEARCH FILTER **/
+ // if (search?.trim()) {
+ // const term = search.trim();
+
+ // if (/^\d+$/.test(term)) {
+ // filters.id = Number(term);
+ // } else {
+ // filters.user = {
+ // ...filters.user,
+ // OR: [
+ // { emailAddress: { contains: term, mode: 'insensitive' } },
+ // { firstName: { contains: term, mode: 'insensitive' } },
+ // { lastName: { contains: term, mode: 'insensitive' } },
+ // ],
+ // };
+ // }
+ // }
if (search?.trim()) {
const term = search.trim();
-
- if (/^\d+$/.test(term)) {
- filters.id = Number(term);
- } else {
- filters.user = {
- ...filters.user,
+ filters.AND = [
+ {
OR: [
- { emailAddress: { contains: term, mode: 'insensitive' } },
- { firstName: { contains: term, mode: 'insensitive' } },
- { lastName: { contains: term, mode: 'insensitive' } },
+ {
+ companyName: {
+ contains: term,
+ mode: 'insensitive',
+ },
+ },
+ {
+ user: {
+ OR: [
+ {
+ firstName: {
+ contains: term,
+ mode: 'insensitive',
+ },
+ },
+ {
+ lastName: {
+ contains: term,
+ mode: 'insensitive',
+ },
+ },
+ {
+ userRefNumber: {
+ contains: term,
+ mode: 'insensitive',
+ },
+ },
+ ],
+ },
+ },
],
- };
- }
+ },
+ ];
}
/** USER STATUS FILTER **/
@@ -1416,6 +1462,25 @@ export class MinglarService {
return suggestions;
}
+ async getSuggestionsForAM(hostXid: number) {
+ const suggestions = await this.prisma.hostSuggestion.findMany({
+ where: { hostXid: hostXid, isreviewed: false, isActive: true },
+ select: {
+ id: true,
+ title: true,
+ comments: true,
+ isparent: true,
+ isreviewed: true,
+ reviewOn: true,
+ },
+ orderBy: {
+ id: 'asc',
+ },
+ });
+
+ return suggestions;
+ }
+
async acceptHostApplication(host_xid: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
await this.prisma.hostHeader.update({
diff --git a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts
index 62ddb32..8f50509 100644
--- a/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts
+++ b/src/modules/minglaradmin/services/rejectionMailtoHost.service.ts
@@ -39,6 +39,7 @@ export async function sendEmailToHostForRejectedApplication(
export async function sendAMRejectionMailtoHost(
emailAddress: string,
+ name: string
): Promise<{
sent: boolean;
// messageId: string
@@ -47,11 +48,15 @@ export async function sendAMRejectionMailtoHost(
const subject = "Improvement of your application";
const htmlContent = `
- Dear Host,
+ Dear ${name},
Your account manager has reviewed your application and provided some suggestions.
Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.
You may access your application using the link below:
- Link: ${config.HOST_LINK}
+ Link:
+
+ ${config.HOST_LINK}
+
+
If you have any questions, please feel free to contact the Minglar Support Team.
Best regards,
Minglar Team
@@ -75,3 +80,49 @@ export async function sendAMRejectionMailtoHost(
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
}
}
+
+
+export async function sendAMPQQRejectionMailtoHost(
+ emailAddress: string,
+ name: string
+): Promise<{
+ sent: boolean;
+ // messageId: string
+}> {
+
+ const subject = "Improvement of your activity onboarding application";
+
+ const htmlContent = `
+ Dear ${name},
+
+ Your account manager has reviewed your activity application and provided some suggestions.
+ Please make the necessary improvements and re-submit your activity application along with the pre-qualification answers to proceed with the onboarding process on Minglar.
+
+ You may access your activity onboarding application using the link below:
+ Link: ${config.HOST_LINK}
+
+ If you have any questions, please feel free to contact the Minglar Support Team.
+
+ Best regards,
+ Minglar Team
+
+ `;
+
+ try {
+ const result = await brevoService.sendEmail({
+ recipients: [{ email: emailAddress }],
+ subject,
+ htmlContent,
+ });
+
+ console.log("📧 Email sent successfully:", result);
+
+ return {
+ sent: true,
+ // messageId: result.messageId
+ };
+ } catch (err) {
+ console.error("Brevo email send failed:", err);
+ throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
+ }
+}
diff --git a/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts b/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts
new file mode 100644
index 0000000..c23def5
--- /dev/null
+++ b/src/modules/prepopulate/handlers/getAddActivityPrePopulate.ts
@@ -0,0 +1,38 @@
+import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
+import { prismaClient } from '../../../common/database/prisma.lambda.service';
+import { verifyMinglarAdminHostToken } from '../../../common/middlewares/jwt/authForMinglarAdminHost';
+import { safeHandler } from '../../../common/utils/handlers/safeHandler';
+import ApiError from '../../../common/utils/helper/ApiError';
+import { PrePopulateService } from '../services/prepopulate.service';
+
+const prePopulateService = new PrePopulateService(prismaClient);
+
+export const handler = safeHandler(async (
+ event: APIGatewayProxyEvent,
+ context?: Context
+): Promise => {
+ // Extract token from headers
+ const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
+ if (!token) {
+ throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
+ }
+
+ // Authenticate user using the shared authForHost function
+ await verifyMinglarAdminHostToken(token);
+
+ const result = await prePopulateService.getAllPrePopulateDataForAddActivity();
+
+ return {
+ statusCode: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ },
+ body: JSON.stringify({
+ success: true,
+ message: 'Data retrieved successfully',
+ data: result,
+ }),
+ };
+});
+
diff --git a/src/modules/prepopulate/services/prepopulate.service.ts b/src/modules/prepopulate/services/prepopulate.service.ts
index b62e2ae..563fb85 100644
--- a/src/modules/prepopulate/services/prepopulate.service.ts
+++ b/src/modules/prepopulate/services/prepopulate.service.ts
@@ -141,4 +141,62 @@ export class PrePopulateService {
},
});
}
+
+ async getAllPrePopulateDataForAddActivity() {
+ const [
+ foodType,
+ cuisineDetails,
+ vehicleType,
+ navigationMode,
+ taxDetails,
+ energyLevel,
+ aminitiesDetails,
+ allowedEntryType,
+ ageRestrictionDetails
+ ] =
+ await this.prisma.$transaction([
+ this.prisma.foodTypes.findMany({
+ where: { isActive: true },
+ orderBy: { foodTypeName: 'asc' },
+ }),
+ this.prisma.foodCuisines.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.transportModes.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.navigationModes.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.taxes.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.energyLevels.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.amenities.findMany({
+ where: { isActive: true },
+ }),
+ this.prisma.allowedEntryTypes.findMany({
+ where: { isActive: true },
+ orderBy: { allowedEntryTypeName: 'asc' }
+ }),
+ this.prisma.ageRestrictions.findMany({
+ where: { isActive: true },
+ orderBy: { ageRestrictionName: 'asc' }
+ }),
+ ]);
+
+ return {
+ foodType,
+ cuisineDetails,
+ vehicleType,
+ navigationMode,
+ taxDetails,
+ energyLevel,
+ aminitiesDetails,
+ allowedEntryType,
+ ageRestrictionDetails
+ };
+ }
}