42 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
e4a2a04045 fixed the coverimage issue for random activities and addbucket issue also resolved and taking the group count 2026-03-25 13:34:12 +05:30
50ce8e39c5 sending checkin lat long in the user itinerary api and city state country details of the parent company in the getbyid api of host 2026-03-25 12:36:47 +05:30
19e57f0e7f fixed the new user condition in the user module 2026-03-25 11:33:57 +05:30
ad5e343b66 changed the draft-activity to draft 2026-03-24 17:38:07 +05:30
8c3ece6ebd checking the activity title should not be same 2026-03-24 14:42:09 +05:30
092f425bb3 changed the to review 2026-03-24 14:41:25 +05:30
b1a3afd3a1 Sending mail on the final submission of the pqp 2026-03-23 19:31:19 +05:30
97f431260d sending the admin email in the prepopulate 2026-03-23 15:04:05 +05:30
bf6d9ae00b sending created at 2026-03-20 15:15:40 +05:30
518ec4eb21 made save itinerary and get itinerary details apis 2026-03-18 19:49:04 +05:30
95b061b400 made searchConnectionPeople to invite in the itinerary API 2026-03-18 13:30:41 +05:30
92992797ab sending the filter details in the getMatchingBucketInterestedActivities API 2026-03-18 13:21:32 +05:30
c96e3b0c1a sending the energyLevel details also in the getUserItineraryDetails service 2026-03-18 12:45:59 +05:30
f23b93801c making the energylevelxid optional 2026-03-18 11:09:57 +05:30
f1801a3210 made getMatchingBucketInterestedActivities api 2026-03-17 16:22:03 +05:30
2588ca4317 fixed the path 2026-03-17 14:28:56 +05:30
e809ba4480 sending mail after host submits pqp questioniare and not sending the applications with new status in the host applications 2026-03-16 15:50:27 +05:30
678be7c905 fixed the issue of path 2026-03-16 15:27:20 +05:30
08b4231e5f Merge branch 'Split-lambda' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-16 11:30:53 +05:30
a3ab9db5a2 Made getUserItineraryDetails api for the prepopulate data of itinerary page in mobile 2026-03-15 20:33:45 +05:30
32 changed files with 4694 additions and 396 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

@@ -13,7 +13,7 @@ minglarRegistration:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/registration
path: /registration
method: post
minglarLoginForAdmin:
@@ -28,7 +28,7 @@ minglarLoginForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/login
path: /login
method: post
minglarCreatePassword:
@@ -43,7 +43,7 @@ minglarCreatePassword:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/create-password
path: /create-password
method: post
updateMinglarProfile:
@@ -60,7 +60,7 @@ updateMinglarProfile:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/update-profile
path: /update-profile
method: patch
prepopulateRole:
@@ -75,7 +75,7 @@ prepopulateRole:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/prepopulate-Roles
path: /prepopulate-Roles
method: get
getHostDetailsById:
@@ -90,7 +90,7 @@ getHostDetailsById:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-host-details/{host_xid}
path: /hosthub/hosts/get-host-details/{host_xid}
method: get
inviteTeammate:
@@ -105,7 +105,7 @@ inviteTeammate:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/invite-teammate
path: /settings/teammates/invite-teammate
method: post
getAllHostApplication:
@@ -121,7 +121,7 @@ getAllHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-all-host-applications-am
path: /hosthub/hosts/get-all-host-applications-am
method: get
getAllHostActivityForAdmin:
@@ -137,7 +137,7 @@ getAllHostActivityForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/get-all-activity-of-host/{id}
path: /get-all-activity-of-host/{id}
method: get
getAllOnboardingHostApplications:
@@ -153,7 +153,7 @@ getAllOnboardingHostApplications:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin
path: /hosthub/onboarding/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
@@ -169,7 +169,7 @@ getAllOnboardingHostApplications_New:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin-new
path: /hosthub/onboarding/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
@@ -184,7 +184,7 @@ getAllInvitationDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invitation-details
path: /settings/teammates/get-all-invitation-details
method: get
addSuggestion:
@@ -200,7 +200,7 @@ addSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-suggestion
path: /hosthub/hosts/add-suggestion
method: post
getAllCoadminAndAMDetails:
@@ -215,7 +215,7 @@ getAllCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-coadmin-am
path: /settings/teammates/get-all-coadmin-am
method: get
getAllInvitedCoadminAndAMDetails:
@@ -230,7 +230,7 @@ getAllInvitedCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invited-coadmin-am
path: /settings/teammates/get-all-invited-coadmin-am
method: get
getAmDetailsbyId:
@@ -245,7 +245,7 @@ getAmDetailsbyId:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-am-details-by-id/{amXid}
path: /settings/teammates/get-am-details-by-id/{amXid}
method: get
assignAMToHost:
@@ -261,7 +261,7 @@ assignAMToHost:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/assign-am
path: /hosthub/onboarding/assign-am
method: patch
editAgreementDetailsAndAccept:
@@ -277,7 +277,7 @@ editAgreementDetailsAndAccept:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/edit-agreement-accept-host
path: /hosthub/onboarding/edit-agreement-accept-host
method: patch
getAllPqqQuesAnsForAM:
@@ -292,7 +292,7 @@ getAllPqqQuesAnsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-pqq-ques-ans-for-am
path: /hosthub/onboarding/get-all-pqq-ques-ans-for-am
method: get
acceptHostApplication:
@@ -308,7 +308,7 @@ acceptHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-host-application
path: /hosthub/hosts/accept-host-application
method: patch
RejectPQQByAM:
@@ -324,7 +324,7 @@ RejectPQQByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-pq-by-am
path: /hosthub/hosts/reject-pq-by-am
method: patch
rejectActivityDetailsApplicationByAM:
@@ -340,7 +340,7 @@ rejectActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-activity-application-by-am
path: /hosthub/hosts/reject-activity-application-by-am
method: patch
acceptPQByAM:
@@ -356,7 +356,7 @@ acceptPQByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-pq-by-am
path: /hosthub/hosts/accept-pq-by-am
method: patch
acceptActivityDetailsApplicationByAM:
@@ -372,7 +372,7 @@ acceptActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-activity-application-by-am
path: /hosthub/hosts/accept-activity-application-by-am
method: patch
rejectHostApplication:
@@ -388,7 +388,7 @@ rejectHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/reject-host-application
path: /hosthub/onboarding/reject-host-application
method: patch
rejectHostApplicationAM:
@@ -404,7 +404,7 @@ rejectHostApplicationAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-host-application-am
path: /hosthub/hosts/reject-host-application-am
method: patch
addPQQSuggestion:
@@ -420,7 +420,7 @@ addPQQSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
path: /hosthub/hosts/add-Pqq-suggestion
method: post
addActivitySuggestion:
@@ -436,7 +436,7 @@ addActivitySuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Activity-suggestion
path: /hosthub/hosts/add-Activity-suggestion
method: post
getAllPQPDetailsForAM:
@@ -452,7 +452,7 @@ getAllPQPDetailsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
path: /hosthub/pqp/pqp-details-for-am/{activityXid}
method: get
getSuggestionsForAM:
@@ -468,5 +468,5 @@ getSuggestionsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
path: /hosthub/onboarding/show-suggestion-to-am/{hostXid}
method: get

View File

@@ -288,6 +288,21 @@ getActivityFromConnectionsInterest:
path: /connections/get-activity-from-connections-interest
method: get
searchConnectionPeople:
handler: src/modules/user/handlers/connections/searchConnectionPeople.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/connections/**'
- ${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: /connections/search-connection-people
method: get
viewMoreActivitiesByInterest:
handler: src/modules/user/handlers/activities/viewMoreActivities.handler
memorySize: 384
@@ -406,4 +421,109 @@ getAllBucketActivities:
events:
- httpApi:
path: /activities/get-all-bucket-activities
method: get
method: get
getUserItineraryDetails:
handler: src/modules/user/handlers/itinerary/getUserItineraryDetails.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/get-user-itinerary-details
method: get
saveUserItinerary:
handler: src/modules/user/handlers/itinerary/saveUserItinerary.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-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
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/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
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/get-matching-bucket-interested-activities
method: post

View File

@@ -55,7 +55,7 @@ export const ACTIVITY_DISPLAY_STATUS = {
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_DRAFT: 'Draft',
ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'Re-submitted',
NOT_LISTED: 'Not Listed',
@@ -94,7 +94,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_DRAFT: 'Draft',
ACTIVITY_NEW: 'New',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_ENHANCING: 'Enhancing',

View File

@@ -8,6 +8,7 @@ import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHo
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { sendPQPEmailToAM } from '../../../services/sendHostResubmitEmailToAM.service';
const hostService = new HostService(prismaClient);
@@ -177,6 +178,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid)
const details = await hostService.getSuggestionDetails(user.id);
await sendPQPEmailToAM(
details.hostDetails.accountManager.emailAddress,
details.hostDetails.accountManager.firstName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
)
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
return {

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,9 +492,83 @@ 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: {
include: {
select: {
id: true,
logoPath: true,
companyName: true,
address1: true,
address2: true,
cities: {
select: {
id: true,
cityName: true
}
},
states: {
select: {
id: true,
stateName: true
}
},
countries: {
select: {
id: true,
countryName: true
}
},
pinCode: true,
registrationNumber: true,
panNumber: true,
gstNumber: true,
formationDate: true,
companyTypes: {
select: {
id: true,
companyTypeName: true
}
},
websiteUrl: true,
instagramUrl: true,
facebookUrl: true,
linkedinUrl: true,
twitterUrl: true,
HostParenetDocuments: {
select: {
id: true,
@@ -479,6 +601,7 @@ export class HostService {
select: {
id: true,
emailAddress: true,
dateOfBirth: true,
firstName: true,
lastName: true,
mobileNumber: true,
@@ -575,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) {
@@ -595,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
@@ -837,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 },
});
@@ -870,7 +1001,7 @@ export class HostService {
},
});
return true;
return user;
}
async getBankBranchById(bankBranchXid: number) {
@@ -1351,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
@@ -1395,7 +1530,7 @@ export class HostService {
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED;
}
// CASE 2: Admin has rejected but host can resubmit
else if (
@@ -1611,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,
@@ -1752,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,
@@ -1810,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,
@@ -3337,6 +3478,34 @@ export class HostService {
throw new ApiError(404, 'Activity not found');
}
const normalizedActivityTitle =
typeof payload.activityTitle === 'string'
? payload.activityTitle.trim()
: '';
if (normalizedActivityTitle) {
payload.activityTitle = normalizedActivityTitle;
const duplicateActivity = await tx.activities.findFirst({
where: {
id: { not: existingActivity.id },
isActive: true,
activityTitle: {
equals: normalizedActivityTitle,
mode: 'insensitive',
},
},
select: { id: true },
});
if (duplicateActivity) {
throw new ApiError(
400,
'Same activity name already exists. Please choose a different name.',
);
}
}
/* --------------------------------
* 3⃣ STATUS DECISION
* -------------------------------- */

View File

@@ -76,3 +76,41 @@ export async function sendEmailToMinglarAdmin(
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}
export async function sendPQPEmailToAM(
emailAddress: string,
minglarAdminName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = `New Pre-qualification Questionnaire from : ${hostCompanyName}`;
const htmlContent = `
<p>Dear ${minglarAdminName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their pre-qualification questionnaire.</p>
<p>Please review their appliaction and take the necessary action.</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 host via email.");
}
}

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

@@ -34,7 +34,7 @@ const bucket = config.aws.bucketName;
@Injectable()
export class MinglarService {
constructor(private prisma: PrismaService | PrismaClient) {}
constructor(private prisma: PrismaService | PrismaClient) { }
async createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id
@@ -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;
}
@@ -314,6 +314,8 @@ export class MinglarService {
companyName: true,
user: {
select: {
firstName: true,
lastName: true,
userRefNumber: true,
},
},
@@ -375,11 +377,52 @@ export class MinglarService {
const {
paginationService,
} = require('@/common/utils/pagination/pagination.service');
return paginationService.createPaginatedResponse(
let hostDetails = null;
if (hostXid) {
hostDetails = await this.prisma.hostHeader.findUnique({
where: { id: hostXid },
select: {
companyName: true,
user: {
select: {
firstName: true,
lastName: true,
userRefNumber: true,
},
},
},
});
}
const paginatedResponse = paginationService.createPaginatedResponse(
hostActivities,
totalCount,
paginationOptions || { page: 1, limit: 10, skip: 0 },
);
// 👇 ADD THIS BLOCK
if (hostActivities.length === 0 && hostDetails) {
paginatedResponse.data = [
{
id: null,
activityRefNumber: null,
activityTitle: null,
totalScore: null,
activityInternalStatus: null,
activityDisplayStatus: null,
amInternalStatus: null,
amDisplayStatus: null,
createdAt: null,
host: hostDetails,
ActivityAmDetails: [],
activityType: null,
},
];
}
return paginatedResponse;
}
async createUserRevenue(
@@ -818,7 +861,7 @@ export class MinglarService {
if (
userStatus &&
userStatus.trim().toLowerCase() ===
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
) {
filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
}
@@ -832,9 +875,9 @@ export class MinglarService {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.NEW,
},
To_Review: {
Re_Submitted: {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.RE_SUBMITTED,
},
Enhancing: {
internal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
@@ -945,6 +988,7 @@ export class MinglarService {
const where: any = {
isActive: true,
hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] },
adminStatusInternal: { notIn: [MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW] },
};
if (search?.trim()) {
@@ -1187,15 +1231,15 @@ export class MinglarService {
// Build search filter if search term is provided
const searchFilter = search
? {
OR: [
{ email: { contains: search, mode: 'insensitive' as const } },
{ firstName: { contains: search, mode: 'insensitive' as const } },
{ lastName: { contains: search, mode: 'insensitive' as const } },
{
userRefNumber: { contains: search, mode: 'insensitive' as const },
},
],
}
OR: [
{ email: { contains: search, mode: 'insensitive' as const } },
{ firstName: { contains: search, mode: 'insensitive' as const } },
{ lastName: { contains: search, mode: 'insensitive' as const } },
{
userRefNumber: { contains: search, mode: 'insensitive' as const },
},
],
}
: {};
// 1. Fetch all required users (Admin, Co-Admin, AM)
@@ -1711,6 +1755,7 @@ export class MinglarService {
isEmailVerfied: true,
isMobileVerfied: true,
isBiometric: true,
createdAt: true,
userAddressDetails: {
select: {
id: true,
@@ -1826,8 +1871,8 @@ export class MinglarService {
});
});
}
async rejectActivityApplicationByAM(activityId: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
await tx.activities.update({

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

@@ -140,7 +140,9 @@ export class PrePopulateService {
}),
]);
return { documentDetails, countryDetails, stateDetails, companyTypeDetails };
const adminEmail = config.MinglarAdminEmail;
return { documentDetails, countryDetails, stateDetails, companyTypeDetails, adminEmail };
}
async getAllFrequencies() {

View File

@@ -86,9 +86,10 @@ export const handler = safeHandler(
body: JSON.stringify({
success: true,
message: 'Access token generated successfully',
data: {
accessToken: newAccessToken.access.token,
accessTokenExpires: newAccessToken.access.expires,
data: null,
},
}),
};
},

View File

@@ -30,16 +30,17 @@ export const handler = safeHandler(async (
const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findFirst({
where: { mobileNumber: mobileNumber, isActive: true, userStatus: USER_STATUS.ACTIVE },
select: { id: true, userPasscode: true, mobileNumber: true },
select: { id: true, userPasscode: true, mobileNumber: true, firstName: true },
});
let newUserLocal;
let isNewUser = false;
if (user && !user.userPasscode) {
if (user && (!user.userPasscode || !user.firstName)) {
// reuse existing invited user record
newUserLocal = user;
isNewUser = true;
} else if (user) {
// Fully registered user already exists
newUserLocal = user;

View File

@@ -0,0 +1,44 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
const userService = new UserService(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 || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const searchQuery = event.queryStringParameters?.searchQuery ?? '';
const result = await userService.searchConnectionPeople(userId, searchQuery);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Connection people retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,62 @@
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 || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
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,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Saved itineraries retrieved successfully',
data: result,
}),
};
});

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 { 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 payload = {
userLat: Number(body.userLat),
userLong: Number(body.userLong),
startDate: body.startDate,
endDate: body.endDate,
startTime: body.startTime,
endTime: body.endTime,
energyLevelXid:
body.energyLevelXid !== undefined && body.energyLevelXid !== null
? Number(body.energyLevelXid)
: undefined,
entryTypeXid: Number(body.entryTypeXid),
groupCount:
body.groupCount !== undefined && body.groupCount !== null
? Number(body.groupCount)
: undefined,
page: body.page !== undefined ? Number(body.page) : 1,
limit: body.limit !== undefined ? Number(body.limit) : 20,
};
if (
Number.isNaN(payload.userLat) ||
Number.isNaN(payload.userLong) ||
!payload.startDate ||
!payload.endDate ||
!payload.startTime ||
!payload.endTime ||
(payload.energyLevelXid !== undefined &&
Number.isNaN(payload.energyLevelXid)) ||
Number.isNaN(payload.entryTypeXid) ||
(payload.groupCount !== undefined && Number.isNaN(payload.groupCount)) ||
Number.isNaN(payload.page) ||
Number.isNaN(payload.limit)
) {
throw new ApiError(
400,
'userLat, userLong, startDate, endDate, startTime, endTime, entryTypeXid, page and limit are required. energyLevelXid is optional.',
);
}
const result = await itineraryService.getMatchingBucketInterestedActivities(
userId,
payload,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Matching itinerary activities retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,43 @@
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 || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const result = await itineraryService.getUserItineraryDetails(userId);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary details retrieved successfully',
data: result,
}),
};
});

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

@@ -0,0 +1,261 @@
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 activities = Array.isArray(body.activities) ? body.activities : [];
if (!body.startDate || !body.endDate || !body.startTime || !body.endTime) {
throw new ApiError(
400,
'startDate, endDate, startTime and endTime are required.',
);
}
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.modeOfTravel ||
activity.travelTimeBetweenPointsMins === undefined ||
activity.travelTimeBetweenPointsMins === null
) {
throw new ApiError(
400,
'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.',
);
}
}
const payload = {
title: body.title,
startDate: body.startDate,
endDate: body.endDate,
startTime: body.startTime,
endTime: body.endTime,
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) =>
(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)) ||
(activity.paxCount !== undefined && Number.isNaN(activity.paxCount)) ||
(activity.totalAmount !== undefined &&
Number.isNaN(activity.totalAmount)) ||
(activity.locationLat !== undefined &&
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.');
}
const result = await itineraryService.saveUserItinerary(userId, payload);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary saved successfully',
data: result,
}),
};
});

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

View File

@@ -1148,7 +1148,9 @@ export class UserService {
// 6⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE)
// =====================================================
const totalActiveCount = await tx.activities.count({
let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -1157,53 +1159,44 @@ export class UserService {
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
: [-1],
},
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
},
});
let randomActivities: any[] = [];
if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) {
const takeCount = Math.min(5, totalActiveCount);
const randomOffsets = new Set<number>();
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
}
const randomFetched = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
where: {
isActive: true,
activityInternalStatus:
ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus:
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
},
const randomFetched = await tx.activities.findMany({
where: {
id: { in: selectedIds },
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
mediaFileName: true,
},
}),
),
);
},
},
});
randomActivities = await Promise.all(
randomFetched
@@ -1817,7 +1810,9 @@ export class UserService {
RANDOM ACTIVITIES (5 COVER IMAGES)
===================================================== */
const totalActiveCount = await tx.activities.count({
let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -1826,50 +1821,43 @@ export class UserService {
id: {
notIn: safeExcludedIds,
},
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
...excludeUserInterestCondition,
},
select: {
id: true,
},
});
let randomActivities: any[] = [];
if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) {
const takeCount = Math.min(5, totalActiveCount);
const randomOffsets = new Set<number>();
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
}
const randomFetched = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: safeExcludedIds,
},
...excludeUserInterestCondition,
},
const randomFetched = await tx.activities.findMany({
where: {
id: { in: selectedIds },
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
mediaFileName: true,
},
}),
),
);
},
},
});
randomActivities = await Promise.all(
randomFetched
@@ -2863,6 +2851,111 @@ export class UserService {
});
}
async searchConnectionPeople(userXid: number, searchQuery?: string) {
const userConnectionDetails = await this.prisma.connectDetails.findMany({
where: {
userXid,
isActive: true,
deletedAt: null,
},
select: {
schoolCompanyXid: true,
},
});
const schoolCompanyXids = [
...new Set(userConnectionDetails.map((item) => item.schoolCompanyXid)),
];
if (!schoolCompanyXids.length) {
return {
count: 0,
people: [],
};
}
const trimmedSearchQuery = searchQuery?.trim() ?? '';
const connectionPeople = await this.prisma.connectDetails.findMany({
where: {
isActive: true,
deletedAt: null,
schoolCompanyXid: { in: schoolCompanyXids },
userXid: { not: userXid },
user: {
isActive: true,
deletedAt: null,
...(trimmedSearchQuery
? {
OR: [
{
firstName: {
contains: trimmedSearchQuery,
mode: 'insensitive',
},
},
{
lastName: {
contains: trimmedSearchQuery,
mode: 'insensitive',
},
},
],
}
: {}),
},
},
distinct: ['userXid'],
orderBy: {
createdAt: 'desc',
},
take: 10,
select: {
userXid: true,
schoolCompany: {
select: {
id: true,
schoolCompanyName: true,
isSchool: true,
},
},
user: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
},
});
const people = await Promise.all(
connectionPeople.map(async (item) => {
const firstName = item.user.firstName?.trim() ?? '';
const lastName = item.user.lastName?.trim() ?? '';
const fullName = `${firstName} ${lastName}`.trim();
return {
userXid: item.user.id,
fullName,
firstName: item.user.firstName,
lastName: item.user.lastName,
profileImage: item.user.profileImage,
profileImagePresignedUrl: await attachPresignedUrl(
item.user.profileImage,
),
schoolCompany: item.schoolCompany,
};
}),
);
return {
count: people.length,
people,
};
}
async searchSchoolsAndCompanies(searchQuery: string, isSchool: boolean) {
if (!searchQuery) {
throw new ApiError(
@@ -3529,7 +3622,9 @@ export class UserService {
RANDOM ACTIVITIES FROM CONNECTION USERS (5 COVER IMAGES)
===================================================== */
const totalActiveCount = await tx.activities.count({
let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: {
id: { in: connectionActivityIds },
isActive: true,
@@ -3537,47 +3632,42 @@ export class UserService {
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: activityTypeIds },
deletedAt: null,
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
},
});
let randomActivities: any[] = [];
if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) {
const takeCount = Math.min(5, totalActiveCount);
const randomOffsets = new Set<number>();
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
}
const randomFetched = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
where: {
id: { in: connectionActivityIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: activityTypeIds },
deletedAt: null,
},
const randomFetched = await tx.activities.findMany({
where: {
id: { in: selectedIds },
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: "asc" },
take: 1,
select: {
mediaFileName: true,
},
},
mediaFileName: true,
},
}),
),
);
},
},
});
randomActivities = await Promise.all(
randomFetched
@@ -4046,56 +4136,54 @@ export class UserService {
async getFiveRandomActivities() {
return await this.prisma.$transaction(async (tx) => {
// Step 1: Count eligible activities
const totalCount = await tx.activities.count({
const eligibleRandomActivityIds = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
},
});
if (totalCount === 0) return [];
if (eligibleRandomActivityIds.length === 0) return [];
// Step 2: Generate 5 unique random offsets
const takeCount = Math.min(5, totalCount);
const randomOffsets = new Set<number>();
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalCount));
}
// Step 3: Fetch activities using skip (efficient for small limit like 5)
const activities = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
const activities = await tx.activities.findMany({
where: {
id: { in: selectedIds },
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
isCoverImage: true,
},
orderBy: {
displayOrder: 'asc',
},
take: 1,
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: {
isActive: true,
},
orderBy: {
displayOrder: 'asc',
},
take: 1,
select: {
mediaFileName: true,
},
},
mediaFileName: true,
},
})
)
);
},
},
});
// Step 4: Attach presigned URLs
const result = await Promise.all(
@@ -4138,7 +4226,7 @@ export class UserService {
}
const existing = await this.prisma.userBucketInterested.findFirst({
where: { userXid, activityXid },
where: { userXid, activityXid, isActive: true },
});
if (existing) {
@@ -4322,4 +4410,4 @@ export class UserService {
oneDay,
};
}
}
}