Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into swagger

This commit is contained in:
paritosh18
2025-12-22 13:35:24 +05:30
36 changed files with 2357 additions and 461 deletions

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

11
package-lock.json generated
View File

@@ -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"

View File

@@ -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",

View File

@@ -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();

View File

@@ -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")
@@ -400,6 +405,7 @@ model FoodCuisines {
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")
@@ -440,7 +446,8 @@ model FoodTypes {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityFoodDetails ActivityFoodDetails[]
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")
@@ -853,7 +861,7 @@ model Activities {
frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
activityTitle String? @map("activity_title") @db.VarChar(30)
activityDescription String? @map("activity_description") @db.VarChar(80)
activityDescription String? @map("activity_description") @db.VarChar(255)
checkInLat Float? @map("check_in_lat")
checkInLong Float? @map("check_in_long")
checkInAddress String? @map("check_in_address") @db.VarChar(150)
@@ -876,6 +884,13 @@ model Activities {
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,15 +1235,47 @@ 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)
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")
@@ -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")
@@ -1311,17 +1362,21 @@ model ActivityNavigationModesTaxes {
model ActivityPickUpDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
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")
ActivityPickUpTransport ActivityPickUpTransport[]
activities Activities? @relation(fields: [activitiesId], references: [id])
activitiesId Int?
activityPickUpTransportTaxes ActivityPickUpTransportTaxes[]
@@map("activity_pick_up_details")
@@schema("act")
@@ -1329,18 +1384,17 @@ model ActivityPickUpDetails {
model ActivityPickUpTransport {
id Int @id @default(autoincrement())
activityPickUpDetailsXid Int @map("activity_pick_up_details_xid")
activityPickUpDetails ActivityPickUpDetails @relation(fields: [activityPickUpDetailsXid], references: [id], onDelete: Cascade)
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")
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[]
pickupDetails ActivityPickUpDetails[]
@@map("activity_pick_up_transport")
@@schema("act")
@@ -1348,8 +1402,8 @@ model ActivityPickUpTransport {
model ActivityPickUpTransportTaxes {
id Int @id @default(autoincrement())
activityPickUpTransportXid Int @map("activity_pick_up_transport_xid")
activityPickUpTransport ActivityPickUpTransport @relation(fields: [activityPickUpTransportXid], references: [id], onDelete: Cascade)
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")

View File

@@ -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: [

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -92,3 +92,18 @@ getFrequenciesOfActivity:
- httpApi:
path: /prepopulate/get-all-Frequencies
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

View File

@@ -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({
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;
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -1,18 +1,18 @@
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,
@@ -20,8 +20,8 @@ export const STEPPER = {
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6
}
REJECTED: 6,
};
export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
@@ -32,8 +32,16 @@ export const ACTIVITY_INTERNAL_STATUS = {
PQ_FAILED: 'PQ Failed',
PQ_TO_UPDATE: 'PQ To Update',
PQ_SUBMITTED: 'PQ Submitted',
PQ_APPROVED: 'PQ Approved'
}
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',
@@ -44,8 +52,15 @@ export const ACTIVITY_DISPLAY_STATUS = {
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved'
}
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',
@@ -56,8 +71,14 @@ export const ACTIVITY_AM_INTERNAL_STATUS = {
PQ_FAILED: 'PQ Failed',
PQ_REJECTED: 'PQ Rejected',
PQ_TO_REVIEW: 'PQ To Review',
PQ_APPROVED: 'PQ Approved'
}
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',
@@ -69,5 +90,11 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
ENHANCING: 'Enchancing',
NEW: 'New',
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised'
}
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_NEW: 'To Review',
ACTIVITY_ENHANCING: 'Enhancing',
ACTIVITY_NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
};

View File

@@ -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 = {

View File

@@ -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(),

View File

@@ -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 venues 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<typeof CreateActivityDto>;

View File

@@ -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<string, string> = {
'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<APIGatewayProxyResult> => {
/* 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<string, any> = {};
const files: Array<{
buffer: Buffer;
mimeType: string;
fileName: string;
fieldName: string;
}> = [];
await new Promise<void>((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<number, Array<{ buffer: Buffer; mimeType: string; fileName: string }>> = 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,
}),
};
},
);

View File

@@ -17,6 +17,16 @@ import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHo
const hostService = new HostService(prismaClient);
function getExtensionFromMime(mimeType: string) {
const map: Record<string, string> = {
'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,

View File

@@ -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) {

View File

@@ -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,
},
}),
};

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@ export async function resendOtpEmail(
const htmlContent = `
<p>Dear ${role},</p>
<p>Your new OTP is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
`;
try {

View File

@@ -13,9 +13,10 @@ export async function sendOtpEmailForHost(
const htmlContent = `
<p>Dear Host,</p>
<p>Your OTP for registration is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Youre almost all set! 🎉</p>
<p>Enter <strong>${otp}</strong> to wrap your registration.</p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
`;
try {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<APIGatewayProxyResult> => {
// 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,
}),
};
});

View File

@@ -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.

View File

@@ -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 = `
<p>Dear Host,</p>
<p>Dear ${name},</p>
<p>Congratulations, Your application to minglar admin has been approved.</p>
<p>You can start onboarding your activities through the host panel.</p>
<p> You can login to your account using the link below:<br/>
@@ -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 = `
<p>Dear ${name},</p>
<p>Congratulations, Your activity onboarding application to minglar admin has been approved.</p>
<p>You can start adding other details of your activity through the host panel.</p>
<p> You can login to your account using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK} </p>
<p>Best regards,<br/>Minglar Team</p>
`;
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.");
}
}

View File

@@ -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({

View File

@@ -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 = `
<p>Dear Host,</p>
<p>Dear ${name},</p>
<p> Your account manager has reviewed your application and provided some suggestions. <br/>
Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.</p>
<p> You may access your application using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK} </p>
<strong>Link:</strong>
<a href="${config.HOST_LINK}" target="_blank">
${config.HOST_LINK}
</a>
</p>
<p> If you have any questions, please feel free to contact the Minglar Support Team. </p>
<p> Best regards,<br/>
<strong>Minglar Team</strong> </p>
@@ -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 = `
<p>Dear ${name},</p>
<p>Your account manager has reviewed your activity application and provided some suggestions.<br/>
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.</p>
<p>You may access your activity onboarding application using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK}</p>
<p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
<p>Best regards,<br/>
<strong>Minglar Team</strong></p>
`;
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.");
}
}

View File

@@ -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<APIGatewayProxyResult> => {
// 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,
}),
};
});

View File

@@ -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
};
}
}