22 Commits

Author SHA1 Message Date
fcdb813fb7 added the display order to send the itinerary activities in actual order 2026-04-08 19:15:47 +05:30
5239e04621 fixed the error handling for submit company details 2026-04-08 18:42:50 +05:30
fe0a2eb95b made the razorpay apis and model 2026-04-08 18:41:08 +05:30
9df8c5443c saving the start stop details and sending it in the get api for the itinerary 2026-04-08 15:57:36 +05:30
be0667d8e9 sending all the details in the getUserItinerary details api 2026-04-08 14:57:49 +05:30
a44321044f made the api for storing the selections for the itinerary of the user 2026-04-07 19:13:06 +05:30
fcac64e0a9 fix the company logo path issue 2026-04-07 18:45:07 +05:30
6ea2ebe5e1 added the dataconsent flags 2026-04-07 13:36:11 +05:30
388f3079a1 changed teh occurance date to start and end date for the saveUserItinerary creation api and sending all the data of host in the get by id api 2026-04-07 12:00:34 +05:30
6a11c15f39 edited the mail templates 2026-04-04 19:55:48 +05:30
ea461b6056 sending welcome email to host and changed the email template for sending otp 2026-04-04 19:12:28 +05:30
6703dc784d fixed the end time issue to saveuseritinerary 2026-04-03 10:13:56 +05:30
e22c37bc65 fixed the issues of the saveuseritinerary file 2026-04-02 16:03:12 +05:30
d32915c865 made the total amount optional 2026-04-01 19:59:31 +05:30
1c7ad52d0e fixed the indentation 2026-04-01 19:57:42 +05:30
f1f1f199e8 made the total Amount optional 2026-04-01 19:57:08 +05:30
1c32c18e03 Fixed the pax and location address 2026-04-01 19:51:13 +05:30
0d3c71ab5a added the stay and free time in the saveUserItinerary api 2026-04-01 16:09:04 +05:30
50f93bbeae added the delete logo path of the main and parent company 2026-04-01 15:10:10 +05:30
3ce9d1d180 sending the food trainer pick up and navigation details in the get user itinerary api 2026-03-27 11:57:05 +05:30
cb819088a0 fixed the comments string issue 2026-03-25 16:29:13 +05:30
8f5f01c287 sending the date of birth of user under the host getbyid api 2026-03-25 15:16:49 +05:30
21 changed files with 2355 additions and 375 deletions

View File

@@ -70,6 +70,7 @@
"pdf-lib": "^1.17.1",
"pizzip": "^3.2.0",
"prisma": "^7.0.1",
"razorpay": "^2.9.6",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"serverless": "4.24.0",

View File

@@ -2,7 +2,7 @@ generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Lambda Node 18/20 (Amazon Linux) target
previewFeatures = ["multiSchema"]
engineType = "library"
engineType = "library"
}
datasource db {
@@ -12,30 +12,32 @@ datasource db {
}
model User {
id Int @id @default(autoincrement())
firstName String? @map("first_name") @db.VarChar(50)
lastName String? @map("last_name") @db.VarChar(50)
roleXid Int? @map("role_xid")
dateOfBirth DateTime? @map("date_of_birth")
genderName String? @map("gender_name") @db.VarChar(20)
role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict)
emailAddress String? @unique @map("email_address") @db.VarChar(150)
isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc.
mobileNumber String? @unique @map("mobile_number") @db.VarChar(15) // international safe limit
userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords
userPasscode String? @map("user_passcode") @db.VarChar(255) // 46 digit passcode
profileImage String? @map("profile_image") @db.VarChar(500) // S3 key or URL
userLat String? @map("user_lat") @db.VarChar(20) // "-23.44444"
userLong String? @map("user_long") @db.VarChar(20)
userStatus String? @default("pending") @map("user_status") @db.VarChar(20)
isEmailVerfied Boolean? @default(false) @map("is_email_verified")
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isProfileUpdated Boolean? @default(false) @map("is_profile_updated")
userRefNumber String? @unique @map("user_ref_number") @db.VarChar(20)
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())
firstName String? @map("first_name") @db.VarChar(50)
lastName String? @map("last_name") @db.VarChar(50)
roleXid Int? @map("role_xid")
dateOfBirth DateTime? @map("date_of_birth")
genderName String? @map("gender_name") @db.VarChar(20)
role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict)
emailAddress String? @unique @map("email_address") @db.VarChar(150)
isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc.
mobileNumber String? @unique @map("mobile_number") @db.VarChar(15) // international safe limit
userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords
userPasscode String? @map("user_passcode") @db.VarChar(255) // 46 digit passcode
profileImage String? @map("profile_image") @db.VarChar(500) // S3 key or URL
userLat String? @map("user_lat") @db.VarChar(20) // "-23.44444"
userLong String? @map("user_long") @db.VarChar(20)
userStatus String? @default("pending") @map("user_status") @db.VarChar(20)
isEmailVerfied Boolean? @default(false) @map("is_email_verified")
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isProfileUpdated Boolean? @default(false) @map("is_profile_updated")
userRefNumber String? @unique @map("user_ref_number") @db.VarChar(20)
dataConsentAccepted Boolean? @default(false) @map("data_consent_accepted")
dataConsentAcceptedOn DateTime? @map("data_consent_accepted_on")
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")
// Relations
UserOtp UserOtp[]
@@ -59,6 +61,7 @@ model User {
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
paymentOrders PaymentOrders[]
inviteDetails InviteDetails[] @relation("InvitedUser")
invitedInviteDetails InviteDetails[] @relation("InviterUser")
userRevenues UserRevenue[]
@@ -1358,15 +1361,16 @@ model ActivityFoodCost {
}
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")
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")
itineraryActivitySelectionFoodTypes ItineraryActivitySelectionFoodType[]
@@map("activity_food_types")
@@schema("act")
@@ -1405,18 +1409,19 @@ model ActivityFoodTaxes {
}
model ActivityEquipments {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
equipmentName String @map("equipment_name") @db.VarChar(30)
isEquipmentChargeable Boolean @default(false) @map("is_equipment_chargeable")
equipmentBasePrice Int @map("equipment_base_price")
equipmentTotalPrice Int @map("equipment_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")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
equipmentName String @map("equipment_name") @db.VarChar(30)
isEquipmentChargeable Boolean @default(false) @map("is_equipment_chargeable")
equipmentBasePrice Int @map("equipment_base_price")
equipmentTotalPrice Int @map("equipment_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")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
itineraryActivitySelectionEquipments ItineraryActivitySelectionEquipment[]
@@map("activity_equipments")
@@schema("act")
@@ -1452,6 +1457,7 @@ model ActivityNavigationModes {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityNavigationModesTaxes ActivityNavigationModesTaxes[]
ItineraryActivitySelections ItineraryActivitySelection[]
@@map("activity_navigation_modes")
@@schema("act")
@@ -1655,29 +1661,31 @@ model ItineraryHeader {
ItineraryMembers ItineraryMembers[]
ItineraryStartStopDetails ItineraryStartStopDetails[]
ItineraryActivities ItineraryActivities[]
paymentOrders PaymentOrders[]
@@map("itinerary_header")
@@schema("itn")
}
model ItineraryMembers {
id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
memberXid Int @map("member_xid")
member User @relation("MemberUser", fields: [memberXid], references: [id], onDelete: Restrict)
memberRole String @map("member_role") @db.VarChar(30)
memberStatus String @default("pending") @map("member_status") @db.VarChar(30)
invitedByXid Int @map("invited_by_xid")
invitedBy User @relation("InvitedByUser", fields: [invitedByXid], 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")
ItineraryStartStopDetails ItineraryStartStopDetails[]
User User? @relation(fields: [userId], references: [id])
userId Int?
ItineraryDetails ItineraryDetails[]
id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
memberXid Int @map("member_xid")
member User @relation("MemberUser", fields: [memberXid], references: [id], onDelete: Restrict)
memberRole String @map("member_role") @db.VarChar(30)
memberStatus String @default("pending") @map("member_status") @db.VarChar(30)
invitedByXid Int @map("invited_by_xid")
invitedBy User @relation("InvitedByUser", fields: [invitedByXid], 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")
ItineraryStartStopDetails ItineraryStartStopDetails[]
User User? @relation(fields: [userId], references: [id])
userId Int?
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_members")
@@schema("itn")
@@ -1708,41 +1716,124 @@ model ItineraryStartStopDetails {
}
model ItineraryActivities {
id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
itineraryType String @map("itinerary_type") @db.VarChar(30)
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Restrict)
scheduledHeaderXid Int @map("scheduled_header_xid")
scheduledHeader ScheduleHeader @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict)
occurenceDate DateTime @map("occurence_date")
startTime String @map("start_time") @db.VarChar(30)
endTime String @map("end_time") @db.VarChar(30)
endDate DateTime @map("end_date")
venueXid Int @map("venue_xid")
venue ActivityVenues @relation(fields: [venueXid], references: [id], onDelete: Restrict)
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
locationAddress Json? @map("location_address")
travelMode String? @map("travel_mode") @db.VarChar(30)
kmForNextPoint Float? @map("km_for_next_point")
timeForNextPointMins Int? @map("time_for_next_point_mins")
paxCount Int? @map("pax_count")
totalAmount Int? @map("total_amount")
bookingStatus String @default("pending") @map("booking_status") @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")
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
displayOrder Int @default(0) @map("display_order")
itineraryType String @map("itinerary_type") @db.VarChar(30)
activityXid Int? @map("activity_xid")
activity Activities? @relation(fields: [activityXid], references: [id], onDelete: Restrict)
scheduledHeaderXid Int? @map("scheduled_header_xid")
scheduledHeader ScheduleHeader? @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict)
occurenceDate DateTime @map("occurence_date")
startTime String @map("start_time") @db.VarChar(30)
endTime String @map("end_time") @db.VarChar(30)
endDate DateTime @map("end_date")
venueXid Int? @map("venue_xid")
venue ActivityVenues? @relation(fields: [venueXid], references: [id], onDelete: Restrict)
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
locationAddress Json? @map("location_address")
travelMode String? @map("travel_mode") @db.VarChar(30)
kmForNextPoint Float? @map("km_for_next_point")
timeForNextPointMins Int? @map("time_for_next_point_mins")
paxCount Int? @map("pax_count")
totalAmount Int? @map("total_amount")
bookingStatus String @default("pending") @map("booking_status") @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")
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_activities")
@@schema("itn")
}
model PaymentOrders {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
itineraryHeaderXid Int? @map("itinerary_header_xid")
itineraryHeader ItineraryHeader? @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
razorpayOrderId String? @unique @map("razorpay_order_id") @db.VarChar(100)
razorpayPaymentId String? @unique @map("razorpay_payment_id") @db.VarChar(100)
razorpaySignature String? @map("razorpay_signature") @db.VarChar(255)
receipt String @unique @map("receipt") @db.VarChar(100)
amount Int @map("amount")
currency String @default("INR") @map("currency") @db.VarChar(10)
paymentStatus String @default("created") @map("payment_status") @db.VarChar(30)
notes Json? @map("notes")
verifiedAt DateTime? @map("verified_at")
paidAt DateTime? @map("paid_at")
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")
@@index([userXid, itineraryHeaderXid])
@@map("payment_orders")
@@schema("itn")
}
model ItineraryActivitySelection {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")
itineraryActivity ItineraryActivities @relation(fields: [itineraryActivityXid], references: [id], onDelete: Cascade)
itineraryMemberXid Int @map("itinerary_member_xid")
itineraryMember ItineraryMembers @relation(fields: [itineraryMemberXid], references: [id], onDelete: Cascade)
isFoodOpted Boolean @default(false) @map("is_food_opted")
isTrainerOpted Boolean @default(false) @map("is_trainer_opted")
isInActivityNavigationOpted Boolean @default(false) @map("is_in_activity_navigation_opted")
activityNavigationModeXid Int? @map("activity_navigation_mode_xid")
activityNavigationMode ActivityNavigationModes? @relation(fields: [activityNavigationModeXid], 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")
selectedFoodTypes ItineraryActivitySelectionFoodType[]
selectedEquipments ItineraryActivitySelectionEquipment[]
@@unique([itineraryActivityXid, itineraryMemberXid])
@@map("itinerary_activity_selection")
@@schema("itn")
}
model ItineraryActivitySelectionFoodType {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityFoodTypeXid Int @map("activity_food_type_xid")
activityFoodType ActivityFoodTypes @relation(fields: [activityFoodTypeXid], references: [id], onDelete: Cascade)
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")
@@unique([itineraryActivitySelectionXid, activityFoodTypeXid])
@@map("itinerary_activity_selection_food_type")
@@schema("itn")
}
model ItineraryActivitySelectionEquipment {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityEquipmentXid Int @map("activity_equipment_xid")
activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
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")
@@unique([itineraryActivitySelectionXid, activityEquipmentXid])
@@map("itinerary_activity_selection_equipment")
@@schema("itn")
}
model ActivitySOSDetails {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")
@@ -1795,8 +1886,8 @@ model ItineraryDetails {
activityStatus String @map("activity_status") @db.VarChar(30)
isChargeable Boolean @default(false) @map("is_chargeable")
baseAmount Int @map("base_amount")
totalAmount Int @map("total_amount")
itineraryStatus String @map("itinerary_status") @db.VarChar(30)
totalAmount Int? @map("total_amount")
itineraryStatus String? @map("itinerary_status") @db.VarChar(30)
isPaid Boolean @default(false) @map("is_paid")
paidByXid Int? @map("paid_by_xid")
paidBy User? @relation(fields: [paidByXid], references: [id], onDelete: Restrict)

View File

@@ -1,5 +1,5 @@
# Legacy monolith config. For new deployments use serverless.*.yml files.
service: minglarDev
service: minglar
useDotenv: true

View File

@@ -453,6 +453,21 @@ saveUserItinerary:
path: /itinerary/save-user-itinerary
method: post
saveItineraryActivitySelections:
handler: src/modules/user/handlers/itinerary/saveItineraryActivitySelections.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${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: /itinerary/save-itinerary-activity-selections
method: post
getAllUserSavedItineraries:
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
memorySize: 512
@@ -468,6 +483,36 @@ getAllUserSavedItineraries:
path: /itinerary/get-all-user-saved-itineraries
method: get
createRazorpayOrder:
handler: src/modules/user/handlers/payment/createOrder.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${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: /payment/create-order
method: post
verifyRazorpayPayment:
handler: src/modules/user/handlers/payment/verifyPayment.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${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: /payment/verify-payment
method: post
getMatchingBucketInterestedActivities:
handler: src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.handler
memorySize: 512

View File

@@ -13,6 +13,40 @@ const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
function parseMultipartFieldValue(val: string) {
if (val === '' || val === 'null' || val === 'undefined') return null;
const cleaned = val.trim();
const looksLikeJson =
(cleaned.startsWith('{') && cleaned.endsWith('}')) ||
(cleaned.startsWith('[') && cleaned.endsWith(']')) ||
(cleaned.startsWith('"') && cleaned.endsWith('"'));
if (!looksLikeJson) return val;
try {
return JSON.parse(cleaned);
} catch {
return val;
}
}
function normalizeComments(comments: unknown): string | null {
if (comments === null || comments === undefined || comments === '') return null;
const value = String(comments).trim();
if (!value) return null;
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1);
}
return value;
}
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
@@ -122,22 +156,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, val);
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
const cleaned = val.trim();
// If it starts and ends with quotes, remove them
const withoutQuotes =
(cleaned.startsWith('"') && cleaned.endsWith('"'))
? cleaned.slice(1, -1)
: cleaned;
fields[fieldname] = JSON.parse(withoutQuotes);
} catch {
fields[fieldname] = val;
}
}
fields[fieldname] = parseMultipartFieldValue(val);
});
bb.on("close", () => resolve());
@@ -154,7 +173,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
const comments = normalizeComments(fields.comments);
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question");

View File

@@ -4,6 +4,7 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { sendWelcomeEmailToHost } from '../../../services/sendOTPEmail.service';
const hostService = new HostService(prismaClient);
@@ -46,7 +47,8 @@ export const handler = safeHandler(async (
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await hostService.createPassword(user_xid, password);
const result = await hostService.createPassword(user_xid, password);
await sendWelcomeEmailToHost(result.emailAddress);
return {
statusCode: 200,

View File

@@ -4,13 +4,9 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
import { ROLE } from '../../../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { encryptUserId } from '../../../../../common/utils/helper/CodeGenerator';
import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator';
import { HostService } from '../../../services/host.service';
import { sendOtpEmailForHost } from '@/modules/host/services/sendOTPEmail.service';
const hostService = new HostService(prismaClient);
export async function generateHostRefNumber(tx: any) {
const lastrecord = await tx.user.findFirst({
orderBy: {
@@ -45,13 +41,23 @@ export const handler = safeHandler(async (
throw new ApiError(400, 'Email is required');
}
const emailToLowerCase = email.toLowerCase()
const emailToLowerCase = email.trim().toLowerCase();
if (!emailToLowerCase) {
throw new ApiError(400, 'Email is required');
}
// Use a single transaction for user creation/lookup and OTP storage
const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { emailAddress: emailToLowerCase },
select: { emailAddress: true, id: true, userPassword: true },
select: {
emailAddress: true,
id: true,
userPassword: true,
dataConsentAccepted: true,
dataConsentAcceptedOn: true,
},
});
if (user && user.userPassword) {
@@ -93,9 +99,18 @@ export const handler = safeHandler(async (
},
});
const encryptedId = encryptUserId(String(newUserLocal.id));
await tx.user.update({
where: { id: Number(newUserLocal.id) },
data: {
dataConsentAccepted: true,
dataConsentAcceptedOn:
user?.dataConsentAccepted && user?.dataConsentAcceptedOn
? user.dataConsentAcceptedOn
: new Date(),
},
});
return { newUser: newUserLocal, otp, encryptedId };
return { newUser: newUserLocal, otp };
});
if (!transactionResult || !transactionResult.otp) {

View File

@@ -142,6 +142,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
const deleteCompanyLogo =
fields.deleteCompanyLogo === 'true' || fields.deleteCompanyLogo === true;
const deleteParentCompanyLogo =
fields.deleteParentCompanyLogo === 'true' || fields.deleteParentCompanyLogo === true;
/** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -172,14 +176,46 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) {
const { firstName, lastName, mobileNumber } = userProfileRaw;
const firstName =
typeof userProfileRaw.firstName === 'string'
? userProfileRaw.firstName.trim()
: undefined;
const lastName =
typeof userProfileRaw.lastName === 'string'
? userProfileRaw.lastName.trim()
: undefined;
const mobileNumber =
typeof userProfileRaw.mobileNumber === 'string'
? userProfileRaw.mobileNumber.trim()
: undefined;
if (mobileNumber) {
const existingUser = await prismaClient.user.findFirst({
where: {
mobileNumber,
id: {
not: Number(userInfo.id),
},
},
select: {
id: true,
},
});
if (existingUser) {
throw new ApiError(
409,
'Mobile number already exists for another user. Please use a different mobile number.',
);
}
}
await prismaClient.user.update({
where: { id: userInfo.id },
data: {
...(firstName && { firstName }),
...(lastName && { lastName }),
...(mobileNumber && { mobileNumber }),
...(firstName !== undefined && { firstName }),
...(lastName !== undefined && { lastName }),
...(mobileNumber !== undefined && { mobileNumber }),
},
});
}
@@ -379,6 +415,63 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
});
}
/** DELETE EXISTING LOGO IF REQUESTED */
if (deleteCompanyLogo) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: { logoPath: true },
});
if (existingHost?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingHost.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for company logo:', existingHost.logoPath, e);
}
}
parsedCompany.logoPath = null;
}
/** DELETE EXISTING PARENT COMPANY LOGO IF REQUESTED */
if (deleteParentCompanyLogo && parsedCompany.isSubsidairy) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: {
id: true,
hostParent: {
select: {
id: true,
logoPath: true,
},
take: 1,
},
},
});
const existingParent = Array.isArray(existingHost?.hostParent)
? existingHost.hostParent[0]
: existingHost?.hostParent;
if (existingParent?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingParent.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for parent company logo:', existingParent.logoPath, e);
}
}
if (parsedParentCompany) {
parsedParentCompany.logoPath = null;
} else {
parsedParentCompany = {
logoPath: null,
};
}
}
/** UPLOAD LOGO (if provided) */
const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -449,6 +542,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany,
uploadedParentDocs,
isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
);
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

@@ -395,6 +395,17 @@ const s3 = new AWS.S3({
region: config.aws.region,
});
function getS3KeyFromStoredPath(path?: string | null) {
if (!path) return null;
return path.startsWith('http') ? path.split('.com/')[1] || null : path;
}
function resolveIncomingLogoPath(path?: string | null) {
if (typeof path !== 'string') return null;
const trimmed = path.trim();
return trimmed.length ? trimmed : null;
}
type UpdateHostProfileInput = {
firstName?: string;
lastName?: string | null;
@@ -415,6 +426,43 @@ type UpdateHostProfileInput = {
export class HostService {
constructor(private prisma: PrismaClient) { }
private async getValidLogoUrl(
model: 'hostHeader' | 'hostParent',
recordId: number,
logoPath?: string | null,
) {
const key = getS3KeyFromStoredPath(logoPath);
if (!key) return null;
try {
await s3
.headObject({
Bucket: bucket,
Key: key,
})
.promise();
return await getPresignedUrl(bucket, key);
} catch (error: any) {
const statusCode = error?.statusCode;
const errorCode = error?.code;
const isMissingObject =
statusCode === 404 ||
errorCode === 'NotFound' ||
errorCode === 'NoSuchKey';
if (isMissingObject) {
await (this.prisma as any)[model].update({
where: { id: recordId },
data: { logoPath: null },
});
return null;
}
throw error;
}
}
async createHost(data: CreateHostDto) {
return this.prisma.user.create({ data });
}
@@ -444,7 +492,42 @@ export class HostService {
async getHostById(id: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: id },
include: {
select: {
id: true,
logoPath: true,
companyName: true,
address1: true,
address2: true,
pinCode: true,
isSubsidairy: true,
registrationNumber: true,
panNumber: true,
gstNumber: true,
formationDate: true,
companyTypeXid: true,
websiteUrl: true,
instagramUrl: true,
facebookUrl: true,
linkedinUrl: true,
twitterUrl: true,
stepper: true,
hostStatusInternal: true,
hostStatusDisplay: true,
adminStatusInternal: true,
adminStatusDisplay: true,
amStatus: true,
agreementAccepted: true,
assignedOn: true,
agreementStartDate: true,
isApproved: true,
durationNumber: true,
durationFrequency: true,
isCommisionBase: true,
commisionPer: true,
amountPerBooking: true,
payoutDurationNum: true,
payoutDurationFrequency: true,
referencedBy: true,
hostParent: {
select: {
id: true,
@@ -466,8 +549,8 @@ export class HostService {
},
countries: {
select: {
id: true,
countryName: true
id: true,
countryName: true
}
},
pinCode: true,
@@ -518,6 +601,7 @@ export class HostService {
select: {
id: true,
emailAddress: true,
dateOfBirth: true,
firstName: true,
lastName: true,
mobileNumber: true,
@@ -614,11 +698,15 @@ export class HostService {
}
if (host?.logoPath) {
const key = host.logoPath.startsWith('http')
? host.logoPath.split('.com/')[1]
: host.logoPath;
host.logoPath = await getPresignedUrl(bucket, key);
const resolvedLogoUrl = await this.getValidLogoUrl(
'hostHeader',
host.id,
host.logoPath,
);
if (!resolvedLogoUrl) {
host.logoPath = null;
}
(host as any).logoPresignedUrl = resolvedLogoUrl;
}
if (host.accountManager?.profileImage) {
@@ -634,11 +722,15 @@ export class HostService {
// Parent company logo
if (parent.logoPath) {
const key = parent.logoPath.startsWith('http')
? parent.logoPath.split('.com/')[1]
: parent.logoPath;
parent.logoPath = await getPresignedUrl(bucket, key);
const resolvedParentLogoUrl = await this.getValidLogoUrl(
'hostParent',
parent.id,
parent.logoPath,
);
if (!resolvedParentLogoUrl) {
parent.logoPath = null;
}
(parent as any).logoPresignedUrl = resolvedParentLogoUrl;
}
// Parent documents
@@ -876,10 +968,10 @@ export class HostService {
return newUser;
}
async createPassword(user_xid: number, password: string): Promise<boolean> {
async createPassword(user_xid: number, password: string): Promise<Partial<User>> {
// Find user by id
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
where: { id: user_xid, isActive: true },
select: { id: true, emailAddress: true, userPassword: true },
});
@@ -909,7 +1001,7 @@ export class HostService {
},
});
return true;
return user;
}
async getBankBranchById(bankBranchXid: number) {
@@ -1390,6 +1482,10 @@ export class HostService {
parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[],
isDraft: boolean = false,
options?: {
deleteCompanyLogo?: boolean;
deleteParentCompanyLogo?: boolean;
},
) {
return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company
@@ -1650,7 +1746,11 @@ export class HostService {
? { connect: { id: Number(companyData.countryXid) } }
: undefined,
pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath,
logoPath: options?.deleteCompanyLogo
? null
: resolveIncomingLogoPath(companyData.logoPath) ??
existingHostCompany.logoPath ??
null,
isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber,
@@ -1791,9 +1891,10 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.logoPath ||
logoPath: options?.deleteParentCompanyLogo
? null
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
existingParentCompany?.logoPath ??
null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
@@ -1849,9 +1950,10 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.logoPath ||
logoPath: options?.deleteParentCompanyLogo
? null
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
existingParentCompany?.logoPath ??
null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,

View File

@@ -1,5 +1,6 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
import { brevoService } from "../../../common/email/brevoApi";
import ApiError from "../../../common/utils/helper/ApiError";
import config from "../../../config/config";
export async function sendOtpEmailForHost(
emailAddress: string,
@@ -9,14 +10,60 @@ export async function sendOtpEmailForHost(
// messageId: string
}> {
const subject = "OTP for Host Registration";
const subject = "Your Minglar Verification Code";
const htmlContent = `
<p>Dear Host,</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>
<p>Hi there 👋</p>
<p>Heres your verification code to get started:</p>
<p><strong>${otp}</strong></p>
<p>This code is valid for the next 5 minutes.</p>
<p>Once verified, you can continue setting up your Minglar account. If you didnt request this, you can safely ignore this email.</p>
<p>Need help? Reach out to us at info@minglargroup.com.</p>
<p>Warm regards,<br/><strong>Team Minglar</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 host via email.");
}
}
export async function sendWelcomeEmailToHost(
emailAddress: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Get Started as a Minglar Host";
const htmlContent = `
<p>Hi ${emailAddress}</p><br/>
<p>Were excited to have you join Minglar as a host. Welcome aboard! 🌟</p><br/>
<p>To get started and bring your activities live, heres what comes next:</p><br/>
<p><strong>Your next steps:</strong></p><br/>
<p>1. Complete your host profile</p><br/>
<p>2. Complete the pre-qualification process for all your activities</p><br/>
<p>3. Submit your activity details for review</p><br/>
<p>4. Go live and start receiving bookings</p><br/>
<p><strong>👉 Access your Host Portal:</strong></p><br/>
<p>${config.HOST_LINK}</p><br/><br/>
<p>If you need any support along the way, our team is always here to help. You can reach us anytime at info@minglargroup.com.</p><br/>
<p>Were looking forward to seeing your experiences come to life on Minglar.</p><br/>
<p>Warm regards,<br/>Team Minglar</p>
`;
try {

View File

@@ -50,7 +50,7 @@ export const handler = safeHandler(async (
if (!hostDetails?.emailAddress) {
throw new ApiError(404, 'Host details or email address not found');
}
await sendEmailToHostForRejectedApplication(hostDetails.emailAddress)
await sendEmailToHostForRejectedApplication(hostDetails.emailAddress, hostDetails.firstName || 'Host');
return {
statusCode: 200,

View File

@@ -83,15 +83,16 @@ export async function sendAMPQQAcceptanceMailtoHost(
// messageId: string
}> {
const subject = "Approval for your activity onboarding application";
const subject = "Your Activity Has Been Qualified for Onboarding";
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_PQ} </p>
<p>Best regards,<br/>Minglar Team</p>
<p>Hi ${name},</p>
<p>Were pleased to inform you that your activity has been qualified on the Minglar platform.</p>
<p>You can now proceed to complete the details of your activity through the Host portal.</p>
<p>Please click the link below to log in to your account and continue:<br/>
<p><a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a></p>
<p>If you have any questions or need assistance, feel free to reach out at info@minglargroup.com.</p>
<p>Warm regards,<br/><strong>Team Minglar</strong></p>
`;
try {

View File

@@ -144,10 +144,10 @@ export class MinglarService {
async getUserDetails(id: number) {
const hostDetail = await this.prisma.hostHeader.findFirst({
where: { id: id },
where: { id: id, isActive: true },
});
const userDetails = await this.prisma.user.findUnique({
where: { id: hostDetail.userXid },
where: { id: hostDetail.userXid, isActive: true },
});
return userDetails;
}

View File

@@ -4,19 +4,21 @@ import config from "../../../config/config";
export async function sendEmailToHostForRejectedApplication(
emailAddress: string,
firstName: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Rejection for your application";
const subject = "Action Needed: Host Onboarding Application";
const htmlContent = `
<p>Dear Host,</p>
<p>Sorry to say that, But your application to minglar admin has been rejected.</p>
<p>Please update your application and resubmit it.</p>
<p>If you have any questions please contact to minglar admin.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Hi ${firstName},</p><br/><br/>
<p>After reviewing your submission, were unable to proceed at this stage, as some details require further updates.<br/>
We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes.</p><br/><br/>
<p>Host portal login : ${config.HOST_LINK}</p><br/><br/>
<p>We appreciate your interest in Minglar and look forward to reviewing your updated application.</p><br/><br/>
<p>Warm regards,<br/><strong>Team Minglar</strong></p>
`;
try {
@@ -47,21 +49,19 @@ export async function sendAMRejectionMailtoHost(
// messageId: string
}> {
const subject = "Improvement of your application";
const subject = "Action Needed: Host Onboarding Application";
const htmlContent = `
<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>
<a href="${link}" target="_blank">
<p>Hi ${name},</p>
<p>After reviewing your submission, were unable to proceed at this stage, as some details require further updates. <br/>
We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes.</p><br/><br/>
<p><a href="${link}" target="_blank">
${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>
<p>We appreciate your interest in Minglar and look forward to reviewing your updated application.</p>
<p>Warm regards,<br/>
<strong>Team Minglar</strong></p>
`;
try {
@@ -92,20 +92,21 @@ export async function sendAMPQQRejectionMailtoHost(
// messageId: string
}> {
const subject = "Improvement of your activity onboarding application";
const subject = "Action Needed: Activity Pre-qualification";
const htmlContent = `
<p>Dear ${name},</p>
<p>Hi ${name},</p><br/><br/>
<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>Thank you for taking the time to submit your activity pre-qualification detaills on the Minglar platform.<br/><br/>
After reviewing your submission, were unable to approve the application at this stage. However, we encourage you to make the suggested updates and refinements, as many applications are successfully approved after revision.</p><br/><br/>
<p>You may access your activity onboarding application using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK}</p>
<p>You can log in to the Host portal to review the feedback and continue updating your application:<br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p><br/><br/>
<p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
<p>If you need any guidance, feel free to reach out to us at info@minglargroup.com.</p><br/><br/>
<p>We appreciate your interest in partnering with Minglar and look forward to reviewing your updated submission.</p><br/><br/>
<p>Best regards,<br/>
<p>Warm regards,<br/>
<strong>Minglar Team</strong></p>
`;

View File

@@ -26,7 +26,26 @@ export const handler = safeHandler(async (
throw new ApiError(400, 'Invalid user ID');
}
const result = await itineraryService.getAllUserSavedItineraries(userId);
const itineraryHeaderXidRaw =
event.queryStringParameters?.itineraryHeaderXid ?? null;
let itineraryHeaderXid: number | undefined;
if (
itineraryHeaderXidRaw !== null &&
itineraryHeaderXidRaw !== undefined &&
itineraryHeaderXidRaw !== ''
) {
itineraryHeaderXid = Number(itineraryHeaderXidRaw);
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
throw new ApiError(400, 'Invalid itineraryHeaderXid');
}
}
const result = await itineraryService.getAllUserSavedItineraries(
userId,
itineraryHeaderXid,
);
return {
statusCode: 200,

View File

@@ -0,0 +1,98 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
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.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const itineraryActivityXid = Number(body.itineraryActivityXid);
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
throw new ApiError(400, 'itineraryActivityXid is required.');
}
const selectedEquipmentIds = Array.isArray(body.selectedEquipmentIds)
? body.selectedEquipmentIds.map((id: unknown) => Number(id))
: [];
const selectedFoodTypeIds = Array.isArray(body.selectedFoodTypeIds)
? body.selectedFoodTypeIds.map((id: unknown) => Number(id))
: [];
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedEquipmentIds must contain valid ids.');
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedFoodTypeIds must contain valid ids.');
}
const toOptionalId = (value: unknown) => {
if (value === undefined || value === null || value === '') {
return null;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new ApiError(400, 'One or more selected option ids are invalid.');
}
return parsed;
};
const result = await itineraryService.saveItineraryActivitySelections(userId, {
itineraryActivityXid,
isFoodOpted:
body.isFoodOpted === undefined ? false : Boolean(body.isFoodOpted),
selectedFoodTypeIds,
isTrainerOpted:
body.isTrainerOpted === undefined ? false : Boolean(body.isTrainerOpted),
isInActivityNavigationOpted:
body.isInActivityNavigationOpted === undefined
? false
: Boolean(body.isInActivityNavigationOpted),
selectedNavigationModeXid: toOptionalId(body.selectedNavigationModeXid),
selectedEquipmentIds,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary activity selections saved successfully.',
data: result,
}),
};
});

View File

@@ -44,22 +44,88 @@ export const handler = safeHandler(async (
);
}
if (
body.startLocationAddress === undefined ||
body.startLocationAddress === null ||
body.startLocationLat === undefined ||
body.startLocationLat === null ||
body.startLocationLong === undefined ||
body.startLocationLong === null ||
body.endLocationAddress === undefined ||
body.endLocationAddress === null ||
body.endLocationLat === undefined ||
body.endLocationLat === null ||
body.endLocationLong === undefined ||
body.endLocationLong === null
) {
throw new ApiError(
400,
'startLocationAddress, startLocationLat, startLocationLong, endLocationAddress, endLocationLat and endLocationLong are required.',
);
}
if (
(typeof body.startLocationAddress === 'string' &&
!body.startLocationAddress.trim()) ||
(typeof body.endLocationAddress === 'string' &&
!body.endLocationAddress.trim())
) {
throw new ApiError(400, 'Location addresses cannot be empty.');
}
if (!activities.length) {
throw new ApiError(400, 'At least one activity is required.');
}
for (const activity of activities) {
const itineraryType =
typeof activity.itineraryType === 'string'
? activity.itineraryType.trim().toUpperCase().replace(/\s+/g, '_')
: 'ACTIVITY';
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME';
if (
!activity.activityXid ||
!activity.venueXid ||
!activity.scheduleHeaderXid ||
!activity.modeOfTravel ||
activity.travelTimeBetweenPointsMins === undefined ||
activity.travelTimeBetweenPointsMins === null
) {
throw new ApiError(
400,
'Each activity must include activityXid, venueXid, scheduleHeaderXid, modeOfTravel and travelTimeBetweenPointsMins.',
'Each itinerary item must include modeOfTravel and travelTimeBetweenPointsMins.',
);
}
if (isCustomItineraryType) {
const customStartDate = activity.startDate || activity.occurenceDate;
const customEndDate = activity.endDate || activity.occurenceDate;
if (
!customStartDate ||
!customEndDate ||
!activity.selectedStartTime ||
!activity.selectedEndTime
) {
throw new ApiError(
400,
`${itineraryType} items must include startDate, endDate, selectedStartTime and selectedEndTime.`,
);
}
if (itineraryType === 'STAY' && !activity.locationAddress) {
throw new ApiError(
400,
'STAY items must include locationAddress.',
);
}
continue;
}
if (!activity.activityXid || !activity.venueXid || !activity.scheduleHeaderXid) {
throw new ApiError(
400,
'ACTIVITY items must include activityXid, venueXid and scheduleHeaderXid.',
);
}
}
@@ -70,49 +136,95 @@ export const handler = safeHandler(async (
endDate: body.endDate,
startTime: body.startTime,
endTime: body.endTime,
activities: activities.map((activity: any) => ({
activityXid: Number(activity.activityXid),
venueXid: Number(activity.venueXid),
scheduleHeaderXid: Number(activity.scheduleHeaderXid),
modeOfTravel: activity.modeOfTravel,
travelTimeBetweenPointsMins: Number(
activity.travelTimeBetweenPointsMins,
),
kmForNextPoint:
activity.kmForNextPoint !== undefined &&
activity.kmForNextPoint !== null
? Number(activity.kmForNextPoint)
: undefined,
occurenceDate: activity.occurenceDate,
selectedStartTime: activity.selectedStartTime,
selectedEndTime: activity.selectedEndTime,
itineraryType: activity.itineraryType,
paxCount:
activity.paxCount !== undefined && activity.paxCount !== null
? Number(activity.paxCount)
: undefined,
totalAmount:
activity.totalAmount !== undefined && activity.totalAmount !== null
? Number(activity.totalAmount)
: undefined,
locationLat:
activity.locationLat !== undefined && activity.locationLat !== null
? Number(activity.locationLat)
: undefined,
locationLong:
activity.locationLong !== undefined && activity.locationLong !== null
? Number(activity.locationLong)
: undefined,
locationAddress: activity.locationAddress,
})),
startLocationAddress: body.startLocationAddress,
startLocationLat:
body.startLocationLat !== null && body.startLocationLat !== undefined
? Number(body.startLocationLat)
: undefined,
startLocationLong:
body.startLocationLong !== null && body.startLocationLong !== undefined
? Number(body.startLocationLong)
: undefined,
endLocationAddress: body.endLocationAddress,
endLocationLat:
body.endLocationLat !== null && body.endLocationLat !== undefined
? Number(body.endLocationLat)
: undefined,
endLocationLong:
body.endLocationLong !== null && body.endLocationLong !== undefined
? Number(body.endLocationLong)
: undefined,
activities: activities.map((activity: any) => {
const itineraryType =
typeof activity.itineraryType === 'string'
? activity.itineraryType.trim().toUpperCase().replace(/\s+/g, '_')
: 'ACTIVITY';
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME';
return {
activityXid:
!isCustomItineraryType &&
activity.activityXid !== undefined &&
activity.activityXid !== null
? Number(activity.activityXid)
: undefined,
venueXid:
!isCustomItineraryType &&
activity.venueXid !== undefined &&
activity.venueXid !== null
? Number(activity.venueXid)
: undefined,
scheduleHeaderXid:
!isCustomItineraryType &&
activity.scheduleHeaderXid !== undefined &&
activity.scheduleHeaderXid !== null
? Number(activity.scheduleHeaderXid)
: undefined,
modeOfTravel: activity.modeOfTravel,
travelTimeBetweenPointsMins: Number(
activity.travelTimeBetweenPointsMins,
),
kmForNextPoint:
activity.kmForNextPoint !== undefined &&
activity.kmForNextPoint !== null
? Number(activity.kmForNextPoint)
: undefined,
startDate: activity.startDate ?? activity.occurenceDate,
endDate: activity.endDate ?? activity.occurenceDate,
occurenceDate: activity.occurenceDate,
selectedStartTime: activity.selectedStartTime,
selectedEndTime: activity.selectedEndTime,
itineraryType,
paxCount:
activity.paxCount !== undefined && activity.paxCount !== null
? Number(activity.paxCount)
: undefined,
totalAmount:
activity.totalAmount !== undefined && activity.totalAmount !== null
? Number(activity.totalAmount)
: undefined,
locationLat:
activity.locationLat !== undefined && activity.locationLat !== null
? Number(activity.locationLat)
: undefined,
locationLong:
activity.locationLong !== undefined && activity.locationLong !== null
? Number(activity.locationLong)
: undefined,
locationAddress: activity.locationAddress,
};
}),
};
if (
payload.activities.some(
(activity) =>
Number.isNaN(activity.activityXid) ||
Number.isNaN(activity.venueXid) ||
Number.isNaN(activity.scheduleHeaderXid) ||
(activity.activityXid !== undefined &&
Number.isNaN(activity.activityXid)) ||
(activity.venueXid !== undefined && Number.isNaN(activity.venueXid)) ||
(activity.scheduleHeaderXid !== undefined &&
Number.isNaN(activity.scheduleHeaderXid)) ||
Number.isNaN(activity.travelTimeBetweenPointsMins) ||
(activity.kmForNextPoint !== undefined &&
Number.isNaN(activity.kmForNextPoint)) ||
@@ -123,6 +235,10 @@ export const handler = safeHandler(async (
Number.isNaN(activity.locationLat)) ||
(activity.locationLong !== undefined &&
Number.isNaN(activity.locationLong)),
Number.isNaN(payload.startLocationLat) ||
Number.isNaN(payload.startLocationLong) ||
Number.isNaN(payload.endLocationLat) ||
Number.isNaN(payload.endLocationLong),
)
) {
throw new ApiError(400, 'One or more numeric itinerary values are invalid.');

View File

@@ -0,0 +1,95 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { PaymentService } from '../../services/payment.service';
const paymentService = new PaymentService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
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.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const amount = Number(body.amount);
if (!Number.isFinite(amount) || amount <= 0) {
throw new ApiError(400, 'amount is required and must be greater than 0.');
}
const currency =
typeof body.currency === 'string' && body.currency.trim()
? body.currency.trim().toUpperCase()
: 'INR';
const receipt =
typeof body.receipt === 'string' && body.receipt.trim()
? body.receipt.trim()
: undefined;
const notes =
body.notes && typeof body.notes === 'object' && !Array.isArray(body.notes)
? body.notes
: undefined;
const itineraryHeaderXid =
body.itineraryHeaderXid !== undefined && body.itineraryHeaderXid !== null
? Number(body.itineraryHeaderXid)
: undefined;
if (
itineraryHeaderXid !== undefined &&
(!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0)
) {
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
}
const result = await paymentService.createOrder(userId, {
amount,
currency,
receipt,
notes: {
userId,
...(itineraryHeaderXid !== undefined ? { itineraryHeaderXid } : {}),
...(notes ?? {}),
},
itineraryHeaderXid,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Order created successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,71 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { PaymentService } from '../../services/payment.service';
const paymentService = new PaymentService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
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.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const paymentId =
typeof body.paymentId === 'string' ? body.paymentId.trim() : '';
const orderId =
typeof body.orderId === 'string' ? body.orderId.trim() : '';
const signature =
typeof body.signature === 'string' ? body.signature.trim() : '';
if (!paymentId || !orderId || !signature) {
throw new ApiError(
400,
'paymentId, orderId and signature are required.',
);
}
const result = await paymentService.verifyPayment(userId, {
paymentId,
orderId,
signature,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Payment verified',
data: result,
}),
};
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import crypto from 'crypto';
import Razorpay from 'razorpay';
import ApiError from '../../../common/utils/helper/ApiError';
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
@Injectable()
export class PaymentService {
constructor(private prisma: PrismaClient) {}
async createOrder(
userXid: number,
payload: {
amount: number;
currency?: string;
receipt?: string;
notes?: Record<string, string | number | boolean | null>;
itineraryHeaderXid?: number;
},
) {
if (!process.env.RAZORPAY_KEY_ID || !process.env.RAZORPAY_KEY_SECRET) {
throw new ApiError(500, 'Razorpay credentials are not configured.');
}
if (!Number.isFinite(payload.amount) || payload.amount <= 0) {
throw new ApiError(400, 'amount must be a positive number.');
}
if (
payload.itineraryHeaderXid !== undefined &&
payload.itineraryHeaderXid !== null
) {
const itineraryHeader = await this.prisma.itineraryHeader.findFirst({
where: {
id: payload.itineraryHeaderXid,
ownerXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
if (!itineraryHeader) {
throw new ApiError(
404,
'Itinerary not found for the logged-in user.',
);
}
}
const amountInPaise = Math.round(payload.amount * 100);
const receipt = payload.receipt ?? `receipt_${Date.now()}`;
const currency = payload.currency ?? 'INR';
const order = (await razorpay.orders.create({
amount: amountInPaise,
currency,
receipt,
notes: payload.notes ?? undefined,
} as any)) as any;
const paymentOrder = await this.prisma.paymentOrders.create({
data: {
userXid,
itineraryHeaderXid: payload.itineraryHeaderXid ?? null,
razorpayOrderId: order.id,
receipt: order.receipt ?? receipt,
amount: order.amount ?? amountInPaise,
currency: order.currency ?? currency,
paymentStatus: order.status ?? 'created',
notes: payload.notes
? (payload.notes as Prisma.InputJsonValue)
: null,
isActive: true,
},
});
return {
paymentOrderId: paymentOrder.id,
orderId: order.id,
amount: paymentOrder.amount,
currency: paymentOrder.currency,
receipt: paymentOrder.receipt,
status: paymentOrder.paymentStatus,
};
}
async verifyPayment(
userXid: number,
payload: {
paymentId: string;
orderId: string;
signature: string;
},
) {
if (!process.env.RAZORPAY_KEY_SECRET) {
throw new ApiError(500, 'Razorpay credentials are not configured.');
}
const paymentId = payload.paymentId?.trim();
const orderId = payload.orderId?.trim();
const signature = payload.signature?.trim();
if (!paymentId || !orderId || !signature) {
throw new ApiError(
400,
'paymentId, orderId and signature are required.',
);
}
const generatedSignature = crypto
.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
.update(`${orderId}|${paymentId}`)
.digest('hex');
if (generatedSignature !== signature) {
throw new ApiError(400, 'Invalid signature.');
}
const paymentOrder = await this.prisma.paymentOrders.findFirst({
where: {
razorpayOrderId: orderId,
userXid,
isActive: true,
deletedAt: null,
},
});
if (!paymentOrder) {
throw new ApiError(404, 'Payment order not found.');
}
if (
paymentOrder.paymentStatus === 'paid' &&
paymentOrder.razorpayPaymentId === paymentId
) {
return {
paymentOrderId: paymentOrder.id,
orderId: paymentOrder.razorpayOrderId,
paymentId: paymentOrder.razorpayPaymentId,
status: paymentOrder.paymentStatus,
verifiedAt: paymentOrder.verifiedAt,
paidAt: paymentOrder.paidAt,
};
}
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
where: {
id: paymentOrder.id,
},
data: {
razorpayPaymentId: paymentId,
razorpaySignature: signature,
paymentStatus: 'paid',
verifiedAt: new Date(),
paidAt: new Date(),
},
});
return {
paymentOrderId: updatedPaymentOrder.id,
orderId: updatedPaymentOrder.razorpayOrderId,
paymentId: updatedPaymentOrder.razorpayPaymentId,
status: updatedPaymentOrder.paymentStatus,
verifiedAt: updatedPaymentOrder.verifiedAt,
paidAt: updatedPaymentOrder.paidAt,
};
}
}