Compare commits
47 Commits
8fccc62f33
...
paritosh-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d18a77ab5 | ||
|
|
e6ba52520d | ||
|
|
e164e1c25a | ||
|
|
ce9a9b6211 | ||
|
|
4e04781a06 | ||
|
|
e77d9e50c9 | ||
|
|
be9780f9ec | ||
|
|
3c56c45b01 | ||
|
|
d1c4ad76ba | ||
|
|
75025b62d9 | ||
|
|
04ae88b239 | ||
|
|
b18b6bc468 | ||
|
|
6f1504e93f | ||
|
|
1a6411acdc | ||
|
|
841539b8cc | ||
|
|
5fcff67916 | ||
|
|
acd31725ed | ||
|
|
0d96b1e67e | ||
|
|
f98354a1c8 | ||
|
|
66d65c3b84 | ||
|
|
eef9bbf368 | ||
|
|
2eac865c51 | ||
| f205dfedd6 | |||
| 22e2e8e1b7 | |||
|
|
ce1be2c94e | ||
|
|
d793852d63 | ||
|
|
9b95149469 | ||
|
|
1789084685 | ||
|
|
cf5bd02a04 | ||
| 0be293288d | |||
|
|
0ba056af98 | ||
| 9e8d9502ae | |||
| 94454a6f25 | |||
|
|
631ae79277 | ||
| 25b82bee31 | |||
|
|
dcb2259c7d | ||
| c5dcc5b1f0 | |||
| b47e6271a3 | |||
| 958a3e5cec | |||
| 181f32b2e7 | |||
| c5cad4fdce | |||
| daff265584 | |||
| 0f5061a129 | |||
| 5e87ab84d1 | |||
| 54a4f22d2f | |||
| bb87e0ac05 | |||
|
|
01569670b4 |
@@ -64,6 +64,8 @@ model User {
|
||||
paymentOrders PaymentOrders[]
|
||||
inviteDetails InviteDetails[] @relation("InvitedUser")
|
||||
invitedInviteDetails InviteDetails[] @relation("InviterUser")
|
||||
hostMembers HostMembers[] @relation("HostMemberUser")
|
||||
invitedHostMembers HostMembers[] @relation("HostMemberInviter")
|
||||
userRevenues UserRevenue[]
|
||||
userInterests UserInterests[]
|
||||
connectDetails ConnectDetails[]
|
||||
@@ -79,6 +81,9 @@ model User {
|
||||
// 🔹 Activities where this user is Account Manager
|
||||
managedActivities Activities[] @relation("ActivityAccountManager")
|
||||
activitySortings ActivitySorting[]
|
||||
sentActivityMessages ActivityMessages[] @relation("ActivityMessageSender")
|
||||
receivedActivityMessages ActivityMessages[] @relation("ActivityMessageReceiver")
|
||||
chatConnections ChatConnections[]
|
||||
|
||||
@@map("users")
|
||||
@@schema("usr")
|
||||
@@ -674,6 +679,8 @@ model Roles {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
User User[]
|
||||
hostMembers HostMembers[] @relation("HostMemberRole")
|
||||
hostRolePermissionMasters HostRolePermissionMasters[] @relation("HostRolePermissionMasterRole")
|
||||
|
||||
@@map("roles")
|
||||
@@schema("mst")
|
||||
@@ -732,6 +739,22 @@ model Token {
|
||||
@@schema("usr")
|
||||
}
|
||||
|
||||
model ChatConnections {
|
||||
id Int @id @default(autoincrement())
|
||||
userXid Int @map("user_xid")
|
||||
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
|
||||
activityXid Int? @map("activity_xid")
|
||||
connectionId String @unique @map("connection_id") @db.VarChar(200)
|
||||
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, activityXid])
|
||||
@@map("chat_connections")
|
||||
@@schema("usr")
|
||||
}
|
||||
|
||||
//HOST MODELS
|
||||
|
||||
model HostHeader {
|
||||
@@ -790,6 +813,8 @@ model HostHeader {
|
||||
HostBankDetails HostBankDetails[]
|
||||
HostDocuments HostDocuments[]
|
||||
HostSuggestion HostSuggestion[]
|
||||
hostMembers HostMembers[]
|
||||
hostRolePermissionMasters HostRolePermissionMasters[]
|
||||
hostParent HostParent[]
|
||||
HostTrack HostTrack[]
|
||||
Activities Activities[]
|
||||
@@ -821,13 +846,90 @@ model HostBankDetails {
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostMembers {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
userXid Int @map("user_xid")
|
||||
user User @relation("HostMemberUser", fields: [userXid], references: [id], onDelete: Cascade)
|
||||
roleXid Int @map("role_xid")
|
||||
role Roles @relation("HostMemberRole", fields: [roleXid], references: [id], onDelete: Restrict)
|
||||
hostRolePermissionMasterXid Int? @map("host_role_permission_master_xid")
|
||||
hostRolePermissionMaster HostRolePermissionMasters? @relation(fields: [hostRolePermissionMasterXid], references: [id], onDelete: Restrict)
|
||||
memberStatus String @default("invited") @map("member_status") @db.VarChar(20)
|
||||
invitedByXid Int? @map("invited_by_xid")
|
||||
invitedBy User? @relation("HostMemberInviter", fields: [invitedByXid], references: [id], onDelete: Restrict)
|
||||
invitedOn DateTime @default(now()) @map("invited_on")
|
||||
acceptedOn DateTime? @map("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")
|
||||
managedActivities HostMemberActivities[]
|
||||
|
||||
@@unique([hostXid, userXid])
|
||||
@@map("host_members")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostRolePermissionMasters {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
roleXid Int @map("role_xid")
|
||||
role Roles @relation("HostRolePermissionMasterRole", fields: [roleXid], references: [id], onDelete: Restrict)
|
||||
permissionMasterXids Json @map("permission_master_xids")
|
||||
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")
|
||||
hostMembers HostMembers[]
|
||||
|
||||
@@unique([hostXid, roleXid])
|
||||
@@map("host_role_permission_masters")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostPermissionMasters {
|
||||
id Int @id @default(autoincrement())
|
||||
permissionKey String @unique @map("permission_key") @db.VarChar(120)
|
||||
permissionGroup String @map("permission_group") @db.VarChar(80)
|
||||
permissionSection String @map("permission_section") @db.VarChar(80)
|
||||
permissionAction String @map("permission_action") @db.VarChar(20)
|
||||
displayLabel String @map("display_label") @db.VarChar(120)
|
||||
displayOrder Int @map("display_order")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("host_permission_masters")
|
||||
@@schema("mst")
|
||||
}
|
||||
|
||||
model HostMemberActivities {
|
||||
id Int @id @default(autoincrement())
|
||||
hostMemberXid Int @map("host_member_xid")
|
||||
hostMember HostMembers @relation(fields: [hostMemberXid], references: [id], onDelete: Cascade)
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], 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([hostMemberXid, activityXid])
|
||||
@@map("host_member_activities")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostDocuments {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
documentTypeXid Int @map("document_type_xid")
|
||||
documentType DocumentType @relation(fields: [documentTypeXid], references: [id], onDelete: Restrict)
|
||||
documentName String @map("document_name") @db.VarChar(20)
|
||||
documentName String @map("document_name") @db.VarChar(50)
|
||||
filePath String @map("file_path") @db.VarChar(400)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -877,7 +979,10 @@ model HostParent {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
companyName String @map("company_name") @db.VarChar(100)
|
||||
companyName String? @map("company_name") @db.VarChar(100)
|
||||
firstName String? @map("first_name") @db.VarChar(50)
|
||||
lastName String? @map("last_name") @db.VarChar(50)
|
||||
mobileNumber String? @map("mobile_number") @db.VarChar(15)
|
||||
address1 String? @map("address_1") @db.VarChar(150)
|
||||
address2 String? @map("address_2") @db.VarChar(150)
|
||||
cityXid Int? @map("city_xid")
|
||||
@@ -1033,11 +1138,31 @@ model Activities {
|
||||
activityCuisines ActivityCuisine[]
|
||||
activityPickUpTransports ActivityPickUpTransport[]
|
||||
userBucketInterests UserBucketInterested[]
|
||||
activityMessages ActivityMessages[]
|
||||
assignedHostMembers HostMemberActivities[]
|
||||
|
||||
@@map("activities")
|
||||
@@schema("act")
|
||||
}
|
||||
|
||||
model ActivityMessages {
|
||||
id Int @id @default(autoincrement())
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
|
||||
senderXid Int @map("sender_xid")
|
||||
sender User @relation("ActivityMessageSender", fields: [senderXid], references: [id], onDelete: Restrict)
|
||||
receivedXid Int @map("received_xid")
|
||||
received User @relation("ActivityMessageReceiver", fields: [receivedXid], references: [id], onDelete: Restrict)
|
||||
message String @map("message") @db.VarChar(2000)
|
||||
status String @default("unread") @map("status") @db.VarChar(30)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([activityXid, senderXid, receivedXid])
|
||||
@@map("activity_messages")
|
||||
@@schema("act")
|
||||
}
|
||||
|
||||
model ActivityOtherDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
activityXid Int @map("activity_xid")
|
||||
@@ -1654,6 +1779,7 @@ model ItineraryHeader {
|
||||
toDate DateTime @map("to_date")
|
||||
toTime String @map("to_time") @db.VarChar(30)
|
||||
itineraryStatus String @default("draft") @map("itinerary_status") @db.VarChar(30)
|
||||
cancellationReason String? @map("cancellation_reason")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@ -7,7 +7,73 @@ const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
||||
});
|
||||
|
||||
const HOST_PERMISSION_MASTER_SEED = [
|
||||
{ permissionKey: 'profile.company_profile.edit', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Edit', displayLabel: 'Company Profile - Edit', displayOrder: 1 },
|
||||
{ permissionKey: 'profile.company_profile.view', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'View', displayLabel: 'Company Profile - View', displayOrder: 2 },
|
||||
{ permissionKey: 'profile.company_profile.hide', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Hide', displayLabel: 'Company Profile - Hide', displayOrder: 3 },
|
||||
{ permissionKey: 'activity_management.onboarding.edit', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Edit', displayLabel: 'Onboarding - Edit', displayOrder: 4 },
|
||||
{ permissionKey: 'activity_management.onboarding.view', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'View', displayLabel: 'Onboarding - View', displayOrder: 5 },
|
||||
{ permissionKey: 'activity_management.onboarding.hide', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Hide', displayLabel: 'Onboarding - Hide', displayOrder: 6 },
|
||||
{ permissionKey: 'activity_management.scheduling.edit', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Edit', displayLabel: 'Scheduling - Edit', displayOrder: 7 },
|
||||
{ permissionKey: 'activity_management.scheduling.view', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'View', displayLabel: 'Scheduling - View', displayOrder: 8 },
|
||||
{ permissionKey: 'activity_management.scheduling.hide', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Hide', displayLabel: 'Scheduling - Hide', displayOrder: 9 },
|
||||
{ permissionKey: 'activity_management.reservation.edit', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 10 },
|
||||
{ permissionKey: 'activity_management.reservation.view', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 11 },
|
||||
{ permissionKey: 'activity_management.reservation.hide', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 12 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Edit', displayLabel: 'Revenue Statistics - Edit', displayOrder: 13 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'View', displayLabel: 'Revenue Statistics - View', displayOrder: 14 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Hide', displayLabel: 'Revenue Statistics - Hide', displayOrder: 15 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Edit', displayLabel: 'Technical Statistics - Edit', displayOrder: 16 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'View', displayLabel: 'Technical Statistics - View', displayOrder: 17 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Hide', displayLabel: 'Technical Statistics - Hide', displayOrder: 18 },
|
||||
{ permissionKey: 'analytics_statements.reservation.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 19 },
|
||||
{ permissionKey: 'analytics_statements.reservation.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 20 },
|
||||
{ permissionKey: 'analytics_statements.reservation.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 21 },
|
||||
{ permissionKey: 'communication.messages.edit', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Edit', displayLabel: 'Messages - Edit', displayOrder: 22 },
|
||||
{ permissionKey: 'communication.messages.view', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'View', displayLabel: 'Messages - View', displayOrder: 23 },
|
||||
{ permissionKey: 'communication.messages.hide', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Hide', displayLabel: 'Messages - Hide', displayOrder: 24 },
|
||||
{ permissionKey: 'communication.broadcast.edit', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Edit', displayLabel: 'Broadcast - Edit', displayOrder: 25 },
|
||||
{ permissionKey: 'communication.broadcast.view', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'View', displayLabel: 'Broadcast - View', displayOrder: 26 },
|
||||
{ permissionKey: 'communication.broadcast.hide', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Hide', displayLabel: 'Broadcast - Hide', displayOrder: 27 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Edit', displayLabel: 'Creating New Promotions - Edit', displayOrder: 28 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.view', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'View', displayLabel: 'Creating New Promotions - View', displayOrder: 29 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Hide', displayLabel: 'Creating New Promotions - Hide', displayOrder: 30 },
|
||||
{ permissionKey: 'promotions.view_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Edit', displayLabel: 'View Promotions - Edit', displayOrder: 31 },
|
||||
{ permissionKey: 'promotions.view_promotions.view', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'View', displayLabel: 'View Promotions - View', displayOrder: 32 },
|
||||
{ permissionKey: 'promotions.view_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Hide', displayLabel: 'View Promotions - Hide', displayOrder: 33 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.edit', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Edit', displayLabel: 'Inviting New Users - Edit', displayOrder: 34 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.view', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'View', displayLabel: 'Inviting New Users - View', displayOrder: 35 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.hide', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Hide', displayLabel: 'Inviting New Users - Hide', displayOrder: 36 },
|
||||
];
|
||||
|
||||
async function seedHostPermissionMasters() {
|
||||
for (const permission of HOST_PERMISSION_MASTER_SEED) {
|
||||
await prisma.hostPermissionMasters.upsert({
|
||||
where: { permissionKey: permission.permissionKey },
|
||||
update: {
|
||||
permissionGroup: permission.permissionGroup,
|
||||
permissionSection: permission.permissionSection,
|
||||
permissionAction: permission.permissionAction,
|
||||
displayLabel: permission.displayLabel,
|
||||
displayOrder: permission.displayOrder,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
create: {
|
||||
permissionKey: permission.permissionKey,
|
||||
permissionGroup: permission.permissionGroup,
|
||||
permissionSection: permission.permissionSection,
|
||||
permissionAction: permission.permissionAction,
|
||||
displayLabel: permission.displayLabel,
|
||||
displayOrder: permission.displayOrder,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await seedHostPermissionMasters();
|
||||
// ✅ Countries
|
||||
// const india = await prisma.countries.upsert({
|
||||
// where: { countryName: 'India' },
|
||||
|
||||
12
serverless.operator.yml
Normal file
12
serverless.operator.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
service: minglar-operator
|
||||
|
||||
useDotenv: ${file(./serverless/common.yml):useDotenv}
|
||||
params: ${file(./serverless/common.yml):params}
|
||||
provider: ${file(./serverless/common.yml):provider}
|
||||
build: ${file(./serverless/common.yml):build}
|
||||
package: ${file(./serverless/common.yml):package}
|
||||
plugins: ${file(./serverless/common.yml):plugins}
|
||||
custom: ${file(./serverless/common.yml):custom}
|
||||
|
||||
functions:
|
||||
- ${file(./serverless/functions/operator.yml)}
|
||||
@@ -34,6 +34,8 @@ provider:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
minimumCompressionSize: 1024
|
||||
websocketsApiName: minglar-ws-${sls:stage}
|
||||
websocketsApiRouteSelectionExpression: $request.body.action
|
||||
|
||||
environment:
|
||||
DATABASE_URL: ${env:DATABASE_URL}
|
||||
@@ -65,6 +67,9 @@ provider:
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
|
||||
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
|
||||
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
|
||||
|
||||
iam:
|
||||
role:
|
||||
@@ -78,6 +83,11 @@ provider:
|
||||
Resource:
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- execute-api:ManageConnections
|
||||
Resource:
|
||||
- 'arn:aws:execute-api:${self:provider.region}:*:*/*/@connections/*'
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
@@ -145,9 +155,11 @@ package:
|
||||
# Import function definitions from separate files organized by module
|
||||
functions:
|
||||
- ${file(./serverless/functions/host.yml)}
|
||||
- ${file(./serverless/functions/operator.yml)}
|
||||
- ${file(./serverless/functions/minglaradmin.yml)}
|
||||
- ${file(./serverless/functions/prepopulate.yml)}
|
||||
- ${file(./serverless/functions/user.yml)}
|
||||
- ${file(./serverless/functions/websocket.yml)}
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
|
||||
@@ -57,8 +57,12 @@ provider:
|
||||
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
|
||||
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
AM_INTERFACE_LINK: ${env:AM_INTERFACE_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
|
||||
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
|
||||
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
|
||||
|
||||
iam:
|
||||
role:
|
||||
|
||||
@@ -308,6 +308,102 @@ updateHostProfile:
|
||||
path: /profile
|
||||
method: patch
|
||||
|
||||
inviteHostMember:
|
||||
handler: src/modules/host/handlers/settings/inviteMember.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/invite-member
|
||||
method: post
|
||||
|
||||
getAllInvitedCoadminAndOperator:
|
||||
handler: src/modules/host/handlers/settings/getAllInvitedCoadminAndOperator.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/invited-coadmin-operators
|
||||
method: get
|
||||
|
||||
saveRolePermissions:
|
||||
handler: src/modules/host/handlers/settings/saveRolePermissions.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/save-role-permissions
|
||||
method: post
|
||||
|
||||
getPermissionMasters:
|
||||
handler: src/modules/host/handlers/settings/getPermissionMasters.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/permission-masters
|
||||
method: get
|
||||
|
||||
getHostMemberRoles:
|
||||
handler: src/modules/host/handlers/settings/getMemberRoles.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/member-roles
|
||||
method: get
|
||||
|
||||
getMemberPermissions:
|
||||
handler: src/modules/host/handlers/settings/getMemberPermissions.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/member-permissions/{memberUserXid}
|
||||
method: get
|
||||
|
||||
# Functions with S3/AWS SDK dependencies
|
||||
submitCompanyDetails:
|
||||
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
|
||||
@@ -431,7 +527,6 @@ resendOTPmail:
|
||||
path: /resend-otp
|
||||
method: post
|
||||
|
||||
|
||||
mediaUploadTos3:
|
||||
handler: src/modules/host/handlers/mediaUploadToS3.handler
|
||||
memorySize: 512
|
||||
@@ -447,7 +542,6 @@ mediaUploadTos3:
|
||||
path: /media/upload/activity/{activityXid}
|
||||
method: post
|
||||
|
||||
|
||||
venueMediaUploadTos3:
|
||||
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
|
||||
memorySize: 512
|
||||
@@ -463,7 +557,6 @@ venueMediaUploadTos3:
|
||||
path: /media/upload/venue/activity/{activityXid}
|
||||
method: post
|
||||
|
||||
|
||||
mediaDeleteFroms3:
|
||||
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
|
||||
memorySize: 512
|
||||
|
||||
198
serverless/functions/operator.yml
Normal file
198
serverless/functions/operator.yml
Normal file
@@ -0,0 +1,198 @@
|
||||
# Operator Module Functions
|
||||
|
||||
operatorSignUp:
|
||||
handler: src/modules/host/handlers/operator/signUp.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||
- 'src/modules/host/services/token.service.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /signup
|
||||
method: post
|
||||
|
||||
operatorVerifyOtp:
|
||||
handler: src/modules/host/handlers/operator/verifyOtp.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||
- 'src/modules/host/services/token.service.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /verify-otp
|
||||
method: post
|
||||
|
||||
operatorCreatePassword:
|
||||
handler: src/modules/host/handlers/operator/createPassword.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||
- 'src/modules/host/services/token.service.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /create-password
|
||||
method: post
|
||||
|
||||
operatorLogin:
|
||||
handler: src/modules/host/handlers/operator/login.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||
- 'src/modules/host/services/token.service.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /login
|
||||
method: post
|
||||
|
||||
operatorVerifyPassword:
|
||||
handler: src/modules/host/handlers/operator/verifyPassword.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /verify-password
|
||||
method: post
|
||||
|
||||
operatorGetActivitiesByDate:
|
||||
handler: src/modules/host/handlers/operator/getActivitiesByDate.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /activities-by-date
|
||||
method: get
|
||||
|
||||
operatorGetReservationByCheckInCode:
|
||||
handler: src/modules/host/handlers/operator/getReservationByCheckInCode.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /reservation-by-checkin-code
|
||||
method: get
|
||||
|
||||
operatorSendOtpCheckIn:
|
||||
handler: src/modules/host/handlers/operator/sendOtpCheckIn.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /send-otp-checkin
|
||||
method: post
|
||||
|
||||
operatorSendOtpCheckout:
|
||||
handler: src/modules/host/handlers/operator/sendOtpCheckout.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /send-otp-checkout
|
||||
method: post
|
||||
|
||||
operatorVerifyOtpCheckIn:
|
||||
handler: src/modules/host/handlers/operator/verifyOtpCheckIn.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /verify-otp-checkin
|
||||
method: post
|
||||
|
||||
operatorVerifyOtpCheckout:
|
||||
handler: src/modules/host/handlers/operator/verifyOtpCheckout.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${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: /verify-otp-checkout
|
||||
method: post
|
||||
@@ -438,6 +438,21 @@ getUserItineraryDetails:
|
||||
path: /itinerary/get-user-itinerary-details
|
||||
method: get
|
||||
|
||||
getItineraryCheckoutDetails:
|
||||
handler: src/modules/user/handlers/itinerary/getItineraryCheckoutDetails.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-itinerary-checkout-details
|
||||
method: get
|
||||
|
||||
saveUserItinerary:
|
||||
handler: src/modules/user/handlers/itinerary/saveUserItinerary.handler
|
||||
memorySize: 512
|
||||
@@ -470,7 +485,7 @@ saveItineraryActivitySelections:
|
||||
|
||||
getAllUserSavedItineraries:
|
||||
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
|
||||
memorySize: 512
|
||||
memorySize: 1024
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/**'
|
||||
@@ -483,6 +498,36 @@ getAllUserSavedItineraries:
|
||||
path: /itinerary/get-all-user-saved-itineraries
|
||||
method: get
|
||||
|
||||
cancelUserItinerary:
|
||||
handler: src/modules/user/handlers/itinerary/cancelUserItinerary.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/cancel-itinerary
|
||||
method: post
|
||||
|
||||
afterBookingFromCalendar:
|
||||
handler: src/modules/user/handlers/itinerary/afterBookingFromCalendar.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/after-booking-from-calendar
|
||||
method: post
|
||||
|
||||
createRazorpayOrder:
|
||||
handler: src/modules/user/handlers/payment/createOrder.handler
|
||||
memorySize: 512
|
||||
@@ -513,6 +558,21 @@ verifyRazorpayPayment:
|
||||
path: /payment/verify-payment
|
||||
method: post
|
||||
|
||||
razorpayWebhook:
|
||||
handler: src/modules/user/handlers/payment/razorpayWebhook.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/webhook/razorpay
|
||||
method: post
|
||||
|
||||
getMatchingBucketInterestedActivities:
|
||||
handler: src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.handler
|
||||
memorySize: 512
|
||||
|
||||
64
serverless/functions/websocket.yml
Normal file
64
serverless/functions/websocket.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
websocketConnect:
|
||||
handler: src/modules/websocket/handlers/connect.handler
|
||||
memorySize: 256
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/websocket/**'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- websocket:
|
||||
route: $connect
|
||||
|
||||
websocketDisconnect:
|
||||
handler: src/modules/websocket/handlers/disconnect.handler
|
||||
memorySize: 256
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/websocket/**'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- websocket:
|
||||
route: $disconnect
|
||||
|
||||
websocketDefault:
|
||||
handler: src/modules/websocket/handlers/default.handler
|
||||
memorySize: 256
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/websocket/**'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- websocket:
|
||||
route: $default
|
||||
|
||||
websocketSendMessage:
|
||||
handler: src/modules/websocket/handlers/sendMessage.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/websocket/**'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- websocket:
|
||||
route: sendMessage
|
||||
|
||||
websocketGetMessages:
|
||||
handler: src/modules/websocket/handlers/getMessages.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/websocket/**'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- websocket:
|
||||
route: getMessages
|
||||
78
src/common/middlewares/jwt/authForAny.ts
Normal file
78
src/common/middlewares/jwt/authForAny.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ROLE } from '@/common/utils/constants/common.constant';
|
||||
import { prisma } from '../../database/prisma.client';
|
||||
|
||||
interface DecodedToken {
|
||||
id?: number;
|
||||
sub?: string | number;
|
||||
role?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export async function verifyAnyToken(
|
||||
token: string
|
||||
): Promise<{ id: number; roleXid: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||
|
||||
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||
if (!userId) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
const latestToken = await prisma.token.findFirst({
|
||||
where: { userXid: userId },
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
|
||||
if (latestToken?.isBlackListed === true) {
|
||||
throw new ApiError(401, 'This session is expired. Please login.');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||
}
|
||||
|
||||
if (user.isActive === false) {
|
||||
throw new ApiError(
|
||||
httpStatus.FORBIDDEN,
|
||||
'Your account is deactivated by admin.'
|
||||
);
|
||||
}
|
||||
|
||||
if (user.roleXid !== ROLE.USER && user.roleXid !== ROLE.HOST) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||
}
|
||||
|
||||
return { id: user.id, roleXid: user.roleXid || 0, role: user.role?.roleName };
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new ApiError(
|
||||
httpStatus.UNAUTHORIZED,
|
||||
'Your session has expired. Please log in again.'
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
httpStatus.FORBIDDEN,
|
||||
'Invalid or expired authentication token.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ROLE } from '@/common/utils/constants/common.constant';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import httpStatus from 'http-status';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import config from '../../../config/config';
|
||||
import { prisma } from '../../database/prisma.client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
|
||||
interface DecodedToken {
|
||||
id?: number;
|
||||
@@ -29,6 +29,68 @@ declare module 'express-serve-static-core' {
|
||||
* Core authentication function - verifies JWT and validates Host user
|
||||
* Can be used by both Express middleware and Lambda handlers
|
||||
*/
|
||||
/**
|
||||
* Verifies JWT and validates Operator user (role_xid = 5)
|
||||
*/
|
||||
export async function verifyOperatorToken(token: string): Promise<{ id: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||
|
||||
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||
}
|
||||
|
||||
// ✅ Fetch user from Prisma (Operator user only)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
const latestToken = await prisma.token.findFirst({
|
||||
where: {
|
||||
userXid: userId
|
||||
},
|
||||
orderBy: { id: 'desc' }
|
||||
})
|
||||
|
||||
if (latestToken?.isBlackListed == true) {
|
||||
throw new ApiError(401, "This session is expired. Please login.")
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||
}
|
||||
|
||||
// ✅ Check if user is active
|
||||
if (user.isActive === false) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
|
||||
}
|
||||
|
||||
// ✅ Check Operator role (role_xid = 5)
|
||||
if (user.roleXid !== ROLE.OPERATOR) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||
}
|
||||
|
||||
return { id: user.id, role: user.role?.roleName };
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
|
||||
112
src/common/middlewares/jwt/authForOperator.ts
Normal file
112
src/common/middlewares/jwt/authForOperator.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '../../database/prisma.client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ROLE } from '../../utils/constants/common.constant';
|
||||
|
||||
interface DecodedToken {
|
||||
id?: number;
|
||||
sub?: string | number;
|
||||
role?: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface UserPayload {
|
||||
id: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyOperatorToken(
|
||||
token: string,
|
||||
): Promise<{ id: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
const latestToken = await prisma.token.findFirst({
|
||||
where: { userXid: userId },
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
|
||||
if (latestToken?.isBlackListed === true) {
|
||||
throw new ApiError(401, 'This session is expired. Please login.');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||
}
|
||||
|
||||
if (user.isActive === false) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
|
||||
}
|
||||
|
||||
if (user.roleXid !== ROLE.OPERATOR) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||
}
|
||||
|
||||
return { id: user.id, role: user.role?.roleName };
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new ApiError(
|
||||
httpStatus.UNAUTHORIZED,
|
||||
'Your session has expired. Please log in again.',
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
|
||||
}
|
||||
}
|
||||
|
||||
const verifyCallback = async (
|
||||
req: Request,
|
||||
resolve: (value?: unknown) => void,
|
||||
reject: (reason?: Error) => void,
|
||||
) => {
|
||||
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||
|
||||
try {
|
||||
const userInfo = await verifyOperatorToken(token);
|
||||
req.user = { id: userInfo.id.toString(), role: userInfo.role };
|
||||
resolve();
|
||||
} catch (error) {
|
||||
return reject(error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
const authForOperator =
|
||||
() =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
verifyCallback(req, resolve, reject);
|
||||
})
|
||||
.then(() => next())
|
||||
.catch((err) => next(err));
|
||||
};
|
||||
|
||||
export default authForOperator;
|
||||
150
src/common/services/chat.service.ts
Normal file
150
src/common/services/chat.service.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
|
||||
interface SendMessageInput {
|
||||
activityXid: number;
|
||||
senderXid: number;
|
||||
receiverXid: number;
|
||||
message: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface GetMessagesInput {
|
||||
activityXid: number;
|
||||
userXid: number;
|
||||
otherUserXid: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class ChatService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
private async getHostUserIdForActivity(activityXid: number): Promise<number> {
|
||||
const activity = await this.prisma.activities.findUnique({
|
||||
where: { id: activityXid },
|
||||
select: { host: { select: { userXid: true } } },
|
||||
});
|
||||
|
||||
if (!activity) {
|
||||
throw new ApiError(404, 'Activity not found');
|
||||
}
|
||||
|
||||
const hostUserXid = activity.host?.userXid;
|
||||
|
||||
if (!hostUserXid) {
|
||||
throw new ApiError(400, 'Host user not found for activity');
|
||||
}
|
||||
|
||||
return hostUserXid;
|
||||
}
|
||||
|
||||
async sendMessage(input: SendMessageInput) {
|
||||
if (!input.activityXid || isNaN(input.activityXid)) {
|
||||
throw new ApiError(400, 'Valid activityXid is required');
|
||||
}
|
||||
|
||||
if (!input.senderXid || isNaN(input.senderXid)) {
|
||||
throw new ApiError(400, 'Valid senderXid is required');
|
||||
}
|
||||
|
||||
if (!input.receiverXid || isNaN(input.receiverXid)) {
|
||||
throw new ApiError(400, 'Valid receiverXid is required');
|
||||
}
|
||||
|
||||
if (input.senderXid === input.receiverXid) {
|
||||
throw new ApiError(400, 'Sender and receiver cannot be the same');
|
||||
}
|
||||
|
||||
const message = input.message?.trim();
|
||||
if (!message) {
|
||||
throw new ApiError(400, 'Message is required');
|
||||
}
|
||||
|
||||
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
|
||||
|
||||
if (input.senderXid !== hostUserXid && input.receiverXid !== hostUserXid) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Sender or receiver must be the host for this activity'
|
||||
);
|
||||
}
|
||||
|
||||
const receiverExists = await this.prisma.user.findUnique({
|
||||
where: { id: input.receiverXid },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!receiverExists) {
|
||||
throw new ApiError(404, 'Receiver not found');
|
||||
}
|
||||
|
||||
return this.prisma.activityMessages.create({
|
||||
data: {
|
||||
activityXid: input.activityXid,
|
||||
senderXid: input.senderXid,
|
||||
receivedXid: input.receiverXid,
|
||||
message,
|
||||
status: input.status?.trim() || 'unread',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMessages(input: GetMessagesInput) {
|
||||
if (!input.activityXid || isNaN(input.activityXid)) {
|
||||
throw new ApiError(400, 'Valid activityXid is required');
|
||||
}
|
||||
|
||||
if (!input.userXid || isNaN(input.userXid)) {
|
||||
throw new ApiError(400, 'Valid userXid is required');
|
||||
}
|
||||
|
||||
if (!input.otherUserXid || isNaN(input.otherUserXid)) {
|
||||
throw new ApiError(400, 'Valid otherUserXid is required');
|
||||
}
|
||||
|
||||
if (input.userXid === input.otherUserXid) {
|
||||
throw new ApiError(400, 'Invalid otherUserXid');
|
||||
}
|
||||
|
||||
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
|
||||
|
||||
if (input.userXid !== hostUserXid && input.otherUserXid !== hostUserXid) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Conversation must include the host for this activity'
|
||||
);
|
||||
}
|
||||
|
||||
const limit = Math.min(Math.max(input.limit || 50, 1), 200);
|
||||
|
||||
const messages = await this.prisma.activityMessages.findMany({
|
||||
where: {
|
||||
activityXid: input.activityXid,
|
||||
OR: [
|
||||
{
|
||||
senderXid: input.userXid,
|
||||
receivedXid: input.otherUserXid,
|
||||
},
|
||||
{
|
||||
senderXid: input.otherUserXid,
|
||||
receivedXid: input.userXid,
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
await this.prisma.activityMessages.updateMany({
|
||||
where: {
|
||||
activityXid: input.activityXid,
|
||||
senderXid: input.otherUserXid,
|
||||
receivedXid: input.userXid,
|
||||
status: 'unread',
|
||||
},
|
||||
data: { status: 'read' },
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
58
src/common/services/websocket.service.ts
Normal file
58
src/common/services/websocket.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export class WebSocketService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async connect(params: {
|
||||
connectionId: string;
|
||||
userXid: number;
|
||||
activityXid?: number | null;
|
||||
}) {
|
||||
const { connectionId, userXid, activityXid } = params;
|
||||
return this.prisma.chatConnections.upsert({
|
||||
where: { connectionId },
|
||||
create: {
|
||||
connectionId,
|
||||
userXid,
|
||||
activityXid: activityXid ?? null,
|
||||
isActive: true,
|
||||
},
|
||||
update: {
|
||||
userXid,
|
||||
activityXid: activityXid ?? null,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect(connectionId: string) {
|
||||
return this.prisma.chatConnections.updateMany({
|
||||
where: { connectionId },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getConnectionById(connectionId: string) {
|
||||
return this.prisma.chatConnections.findFirst({
|
||||
where: { connectionId, isActive: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getConnectionsForUser(params: {
|
||||
userXid: number;
|
||||
activityXid?: number | null;
|
||||
}) {
|
||||
const { userXid, activityXid } = params;
|
||||
return this.prisma.chatConnections.findMany({
|
||||
where: {
|
||||
userXid,
|
||||
isActive: true,
|
||||
...(activityXid ? { activityXid } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export interface OtpResult {
|
||||
export async function resendOtpHelper(
|
||||
prisma: any,
|
||||
userId: number,
|
||||
emailPurpose: "Register" | "Login" | "ForgotPassword",
|
||||
emailPurpose: string,
|
||||
otpLength: 4 | 6 = 4,
|
||||
expiryMinutes: number = 5
|
||||
): Promise<OtpResult> {
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function generateOtpHelper(
|
||||
prisma: any, // ⭐ Inject prisma
|
||||
userId: number,
|
||||
email: string,
|
||||
emailPurpose: "Register" | "Login" | "ForgotPassword",
|
||||
emailPurpose: string,
|
||||
otpLength: 4 | 6 = 4,
|
||||
expiryMinutes: number = 5
|
||||
): Promise<OtpResult> {
|
||||
|
||||
@@ -46,6 +46,18 @@ export const parentCompanySchema = z.object({
|
||||
companyTypeXid: z.number()
|
||||
.optional(),
|
||||
|
||||
firstName: z.string()
|
||||
.max(50, "First name cannot exceed 50 characters")
|
||||
.optional(),
|
||||
|
||||
lastName: z.string()
|
||||
.max(50, "Last name cannot exceed 50 characters")
|
||||
.optional(),
|
||||
|
||||
mobileNumber: z.string()
|
||||
.max(15, "Mobile number cannot exceed 15 characters")
|
||||
.optional(),
|
||||
|
||||
websiteUrl: z.string().nullable().optional(),
|
||||
instagramUrl: z.string().nullable().optional(),
|
||||
facebookUrl: z.string().nullable().optional(),
|
||||
|
||||
@@ -83,8 +83,12 @@ const envVarsSchema = yup
|
||||
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
|
||||
// Email links
|
||||
AM_INVITATION_LINK: yup.string().required('Link to send in AM invitation mail is required'),
|
||||
AM_INTERFACE_LINK:yup.string().required('Link to am interface is required'),
|
||||
HOST_LINK: yup.string().required('Link to host panel is required'),
|
||||
HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required')
|
||||
HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required'),
|
||||
RAZORPAY_KEY_SECRET: yup.string().required('Razorpay key secret is required'),
|
||||
RAZORPAY_KEY_ID: yup.string().required('Razorpay key id is required'),
|
||||
RAZORPAY_WEBHOOK_SECRET: yup.string().required('Razorpay webhook secret is required'),
|
||||
})
|
||||
.noUnknown(true);
|
||||
|
||||
@@ -165,6 +169,11 @@ function getConfig() {
|
||||
AM_INVITATION_LINK: envVars.AM_INVITATION_LINK,
|
||||
HOST_LINK: envVars.HOST_LINK,
|
||||
HOST_LINK_PQ: envVars.HOST_LINK_PQ,
|
||||
RAZORPAY_KEY_ID: envVars.RAZORPAY_KEY_ID,
|
||||
RAZORPAY_KEY_SECRET: envVars.RAZORPAY_KEY_SECRET,
|
||||
RAZORPAY_WEBHOOK_SECRET: envVars.RAZORPAY_WEBHOOK_SECRET,
|
||||
AM_INTERFACE_LINK: envVars.AM_INTERFACE_LINK,
|
||||
|
||||
// oneSignal: {
|
||||
// appID: envVars.ONESIGNAL_APPID,
|
||||
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,
|
||||
|
||||
113
src/modules/host/dto/operator.activity.dto.ts
Normal file
113
src/modules/host/dto/operator.activity.dto.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
export class GetActivitiesByDateRequestDTO {
|
||||
activityDate?: string; // ISO date format: YYYY-MM-DD (optional, defaults to today)
|
||||
}
|
||||
|
||||
export class GetReservationByCheckInCodeRequestDTO {
|
||||
checkInCode!: string;
|
||||
}
|
||||
|
||||
export class OperatorReservationVerificationOtpRequestDTO {
|
||||
checkInCode!: string;
|
||||
}
|
||||
|
||||
export class OperatorReservationVerifyOtpRequestDTO {
|
||||
checkInCode!: string;
|
||||
otp!: string;
|
||||
}
|
||||
|
||||
export class DateBreakdownDTO {
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class ActivitySummaryDTO {
|
||||
activityName: string;
|
||||
activityImage: string | null;
|
||||
activityImagePreSignedUrl: string | null;
|
||||
count: number;
|
||||
dateBreakdown: DateBreakdownDTO[]; // Booking count for each scheduled date
|
||||
}
|
||||
|
||||
export class GetActivitiesByDateResponseDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
date: string;
|
||||
activities: ActivitySummaryDTO[];
|
||||
totalCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class OperatorReservationPersonalDetailsDTO {
|
||||
fullName: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
role: string | null;
|
||||
mobileNumber: string | null;
|
||||
profileImage: string | null;
|
||||
profileImagePreSignedUrl: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export class OperatorReservationBookingInformationDTO {
|
||||
activityName: string | null;
|
||||
slot: string | null;
|
||||
startTime: string | null;
|
||||
endTime: string | null;
|
||||
track: string | null;
|
||||
trackLabel: string | null;
|
||||
date: string | null;
|
||||
dateLabel: string | null;
|
||||
bookedOn: string | null;
|
||||
bookedOnLabel: string | null;
|
||||
}
|
||||
|
||||
export class OperatorReservationBookingIncludedDTO {
|
||||
food: string;
|
||||
selectedFoodTypes: string[];
|
||||
equipment: string;
|
||||
selectedEquipments: string[];
|
||||
trainerOrGuide: string;
|
||||
pickupLocation: string | null;
|
||||
}
|
||||
|
||||
export class OperatorReservationByCheckInCodeDTO {
|
||||
itineraryHeaderXid: number;
|
||||
itineraryActivityXid: number;
|
||||
bookingId: string | null;
|
||||
checkInCode: string | null;
|
||||
reservationStatus: string | null;
|
||||
personalDetails: OperatorReservationPersonalDetailsDTO;
|
||||
bookingInformation: OperatorReservationBookingInformationDTO;
|
||||
bookingIncluded: OperatorReservationBookingIncludedDTO;
|
||||
}
|
||||
|
||||
export class GetReservationByCheckInCodeResponseDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: OperatorReservationByCheckInCodeDTO;
|
||||
}
|
||||
|
||||
export class OperatorReservationVerificationOtpResponseDTO {
|
||||
itineraryActivityXid: number;
|
||||
itineraryMemberXid: number;
|
||||
activityName: string | null;
|
||||
checkInCode: string;
|
||||
verificationType: 'check-in' | 'checkout';
|
||||
requestedChannel: 'email' | 'mobile';
|
||||
deliveryChannel: 'email';
|
||||
destination: string;
|
||||
reservationStatus: string | null;
|
||||
expiresInMinutes: number;
|
||||
deliveryNote: string | null;
|
||||
}
|
||||
|
||||
export class OperatorReservationVerifyOtpResponseDTO {
|
||||
itineraryActivityXid: number;
|
||||
itineraryMemberXid: number;
|
||||
activityName: string | null;
|
||||
checkInCode: string;
|
||||
verificationType: 'check-in' | 'checkout';
|
||||
verified: boolean;
|
||||
reservationStatus: string;
|
||||
}
|
||||
58
src/modules/host/handlers/operator/createPassword.ts
Normal file
58
src/modules/host/handlers/operator/createPassword.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForOperator';
|
||||
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||
|
||||
const operatorAuthService = new OperatorAuthService(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 verifyOperatorToken(token);
|
||||
|
||||
let body: { password?: string; confirmPassword?: string };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { password, confirmPassword } = body;
|
||||
|
||||
if (!password || !confirmPassword) {
|
||||
throw new ApiError(400, 'Password and confirm password are required');
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
throw new ApiError(400, 'Password and confirm password do not match');
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw new ApiError(400, 'Password must be at least 8 characters long');
|
||||
}
|
||||
|
||||
await operatorAuthService.createOperatorPassword(userInfo.id, password);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Password created successfully',
|
||||
data: null,
|
||||
}),
|
||||
};
|
||||
});
|
||||
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { GetActivitiesByDateRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
// Extract token from headers
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Verify token and get operator info
|
||||
const operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
// Get activityDate from query parameters
|
||||
const { activityDate } = event.queryStringParameters || {};
|
||||
|
||||
const requestDTO: GetActivitiesByDateRequestDTO = {
|
||||
activityDate: activityDate?.trim(),
|
||||
};
|
||||
|
||||
// Fetch activities by date and operator
|
||||
const result = await operatorActivityService.getActivitiesByDate(
|
||||
operatorId,
|
||||
requestDTO.activityDate,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Activities fetched successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
// Error will be handled by safeHandler
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { GetReservationByCheckInCodeRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || Number.isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
const requestDTO: GetReservationByCheckInCodeRequestDTO = {
|
||||
checkInCode:
|
||||
event.queryStringParameters?.checkInCode?.trim() ||
|
||||
event.queryStringParameters?.offlineCode?.trim() ||
|
||||
'',
|
||||
};
|
||||
|
||||
if (!requestDTO.checkInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required.');
|
||||
}
|
||||
|
||||
const result = await operatorActivityService.getReservationByCheckInCode(
|
||||
operatorId,
|
||||
requestDTO.checkInCode,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Reservation details fetched successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
55
src/modules/host/handlers/operator/login.ts
Normal file
55
src/modules/host/handlers/operator/login.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { GetHostLoginResponseDTO } from '../../dto/host.dto';
|
||||
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||
import { TokenService } from '../../services/token.service';
|
||||
|
||||
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||
const tokenService = new TokenService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
let body: { emailAddress?: string; userPassword?: string };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { emailAddress, userPassword } = body;
|
||||
|
||||
if (!emailAddress || !userPassword) {
|
||||
throw new ApiError(400, 'Email and password are required');
|
||||
}
|
||||
|
||||
const operator = await operatorAuthService.loginForOperator(
|
||||
emailAddress.trim().toLowerCase(),
|
||||
userPassword,
|
||||
);
|
||||
|
||||
const generatedToken = await tokenService.generateAuthToken(operator.id);
|
||||
|
||||
const response = new GetHostLoginResponseDTO(
|
||||
operator,
|
||||
generatedToken.access.token,
|
||||
generatedToken.refresh.token,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
data: response,
|
||||
}),
|
||||
};
|
||||
});
|
||||
70
src/modules/host/handlers/operator/sendOtpCheckIn.ts
Normal file
70
src/modules/host/handlers/operator/sendOtpCheckIn.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || Number.isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
let body: OperatorReservationVerificationOtpRequestDTO;
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
|
||||
checkInCode: body?.checkInCode?.trim() || '',
|
||||
};
|
||||
|
||||
if (!requestDTO.checkInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required.');
|
||||
}
|
||||
|
||||
const result = await operatorActivityService.sendOtpCheckIn(
|
||||
operatorId,
|
||||
requestDTO.checkInCode,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Check-in OTP sent successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
70
src/modules/host/handlers/operator/sendOtpCheckout.ts
Normal file
70
src/modules/host/handlers/operator/sendOtpCheckout.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || Number.isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
let body: OperatorReservationVerificationOtpRequestDTO;
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
|
||||
checkInCode: body?.checkInCode?.trim() || '',
|
||||
};
|
||||
|
||||
if (!requestDTO.checkInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required.');
|
||||
}
|
||||
|
||||
const result = await operatorActivityService.sendOtpCheckout(
|
||||
operatorId,
|
||||
requestDTO.checkInCode,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Checkout OTP sent successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
53
src/modules/host/handlers/operator/signUp.ts
Normal file
53
src/modules/host/handlers/operator/signUp.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||
|
||||
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
let body: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
emailAddress?: string;
|
||||
isdCode?: string;
|
||||
mobileNumber?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { firstName, lastName, emailAddress, isdCode, mobileNumber } = body;
|
||||
|
||||
if (!emailAddress && !mobileNumber) {
|
||||
throw new ApiError(400, 'Email address or mobile number is required');
|
||||
}
|
||||
|
||||
const signupResponse = await operatorAuthService.signUpOperator({
|
||||
firstName,
|
||||
lastName,
|
||||
emailAddress,
|
||||
isdCode,
|
||||
mobileNumber,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'OTP sent successfully',
|
||||
data: signupResponse,
|
||||
}),
|
||||
};
|
||||
});
|
||||
51
src/modules/host/handlers/operator/verifyOtp.ts
Normal file
51
src/modules/host/handlers/operator/verifyOtp.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||
import { TokenService } from '../../services/token.service';
|
||||
|
||||
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||
const tokenService = new TokenService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
let body: { emailAddress?: string; mobileNumber?: string; otp?: string };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { emailAddress, mobileNumber, otp } = body;
|
||||
|
||||
if ((!emailAddress && !mobileNumber) || !otp) {
|
||||
throw new ApiError(400, 'Email address or mobile number and OTP are required');
|
||||
}
|
||||
|
||||
const operator = await operatorAuthService.verifyOperatorOtp({
|
||||
emailAddress,
|
||||
mobileNumber,
|
||||
otp,
|
||||
});
|
||||
|
||||
const generatedToken = await tokenService.generateAuthToken(operator.id);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'OTP verified successfully',
|
||||
accessToken: generatedToken.access.token,
|
||||
refreshToken: generatedToken.refresh.token,
|
||||
data: null,
|
||||
}),
|
||||
};
|
||||
});
|
||||
72
src/modules/host/handlers/operator/verifyOtpCheckIn.ts
Normal file
72
src/modules/host/handlers/operator/verifyOtpCheckIn.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || Number.isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
let body: OperatorReservationVerifyOtpRequestDTO;
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
|
||||
checkInCode: body?.checkInCode?.trim() || '',
|
||||
otp: body?.otp?.trim() || '',
|
||||
};
|
||||
|
||||
if (!requestDTO.checkInCode || !requestDTO.otp) {
|
||||
throw new ApiError(400, 'checkInCode and otp are required.');
|
||||
}
|
||||
|
||||
const result = await operatorActivityService.verifyOtpCheckIn(
|
||||
operatorId,
|
||||
requestDTO.checkInCode,
|
||||
requestDTO.otp,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Check-in OTP verified successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
72
src/modules/host/handlers/operator/verifyOtpCheckout.ts
Normal file
72
src/modules/host/handlers/operator/verifyOtpCheckout.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || Number.isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
let body: OperatorReservationVerifyOtpRequestDTO;
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
|
||||
checkInCode: body?.checkInCode?.trim() || '',
|
||||
otp: body?.otp?.trim() || '',
|
||||
};
|
||||
|
||||
if (!requestDTO.checkInCode || !requestDTO.otp) {
|
||||
throw new ApiError(400, 'checkInCode and otp are required.');
|
||||
}
|
||||
|
||||
const result = await operatorActivityService.verifyOtpCheckout(
|
||||
operatorId,
|
||||
requestDTO.checkInCode,
|
||||
requestDTO.otp,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Checkout OTP verified successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
44
src/modules/host/handlers/operator/verifyPassword.ts
Normal file
44
src/modules/host/handlers/operator/verifyPassword.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||
|
||||
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
let body: { emailAddress?: string; userPassword?: string };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { emailAddress, userPassword } = body;
|
||||
|
||||
if (!emailAddress || !userPassword) {
|
||||
throw new ApiError(400, 'Email and password are required');
|
||||
}
|
||||
|
||||
const isPasswordValid = await operatorAuthService.verifyPasswordForOperator(
|
||||
emailAddress.trim().toLowerCase(),
|
||||
userPassword,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: isPasswordValid ? 'Password is valid' : 'Password is invalid',
|
||||
data: { isValid: isPasswordValid },
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { paginationService } from '../../../../common/utils/pagination/pagination.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostMemberService } from '../../services/hostMember.service';
|
||||
|
||||
const hostMemberService = new HostMemberService(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 verifyHostToken(token);
|
||||
const search = event.queryStringParameters?.search || '';
|
||||
|
||||
const paginationParams = paginationService.getPaginationFromEvent(event);
|
||||
const paginationOptions =
|
||||
paginationService.parsePaginationParams(paginationParams);
|
||||
|
||||
const { data, totalCount } =
|
||||
await hostMemberService.getAllInvitedCoadminAndOperator({
|
||||
hostUserXid: userInfo.id,
|
||||
search,
|
||||
paginationOptions,
|
||||
});
|
||||
|
||||
const paginatedResponse = paginationService.createPaginatedResponse(
|
||||
data,
|
||||
totalCount,
|
||||
paginationOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Invited co-admin and operator members fetched successfully',
|
||||
...paginatedResponse,
|
||||
}),
|
||||
};
|
||||
});
|
||||
58
src/modules/host/handlers/settings/getMemberPermissions.ts
Normal file
58
src/modules/host/handlers/settings/getMemberPermissions.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostRolePermissionService } from '../../services/hostRolePermission.service';
|
||||
|
||||
const hostRolePermissionService = new HostRolePermissionService(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 verifyHostToken(token);
|
||||
|
||||
const memberUserXid = event.pathParameters?.memberUserXid;
|
||||
|
||||
if (!memberUserXid) {
|
||||
throw new ApiError(400, 'memberUserXid is required.');
|
||||
}
|
||||
|
||||
const memberUserId = parseInt(memberUserXid, 10);
|
||||
if (isNaN(memberUserId)) {
|
||||
throw new ApiError(400, 'Invalid memberUserXid. Must be a number.');
|
||||
}
|
||||
|
||||
const result = await hostRolePermissionService.getMemberPermissions({
|
||||
hostUserXid: userInfo.id,
|
||||
memberUserXid: memberUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Member permissions retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
59
src/modules/host/handlers/settings/getMemberRoles.ts
Normal file
59
src/modules/host/handlers/settings/getMemberRoles.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { ROLE } from '../../../../common/utils/constants/common.constant';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
await verifyHostToken(token);
|
||||
|
||||
const roles = await prismaClient.roles.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [ROLE.CO_ADMIN, ROLE.OPERATOR],
|
||||
},
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Host member roles fetched successfully',
|
||||
data: {
|
||||
roles,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
60
src/modules/host/handlers/settings/getPermissionMasters.ts
Normal file
60
src/modules/host/handlers/settings/getPermissionMasters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
await verifyHostToken(token);
|
||||
|
||||
const permissionMasters = await prismaClient.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Permission masters fetched successfully',
|
||||
data: {
|
||||
permissionMasters,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
111
src/modules/host/handlers/settings/inviteMember.ts
Normal file
111
src/modules/host/handlers/settings/inviteMember.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostMemberService } from '../../services/hostMember.service';
|
||||
import { sendHostMemberInvitationEmail } from '../../services/sendHostMemberInvitationEmail.service';
|
||||
|
||||
const hostMemberService = new HostMemberService(prismaClient);
|
||||
|
||||
interface InviteMemberBody {
|
||||
emailAddress: string;
|
||||
roleXid: number;
|
||||
permissionMasterXid: number;
|
||||
activityXids: number[];
|
||||
}
|
||||
|
||||
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 verifyHostToken(token);
|
||||
|
||||
let body: Partial<InviteMemberBody> = {};
|
||||
if (event.body) {
|
||||
try {
|
||||
body = JSON.parse(event.body);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
const emailAddress =
|
||||
typeof body.emailAddress === 'string' ? body.emailAddress.trim() : '';
|
||||
const roleXid = Number(body.roleXid);
|
||||
const permissionMasterXid = Number(body.permissionMasterXid);
|
||||
const activityXids = Array.isArray(body.activityXids)
|
||||
? body.activityXids
|
||||
: [];
|
||||
|
||||
if (!emailAddress) {
|
||||
throw new ApiError(400, 'emailAddress is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(roleXid) || roleXid <= 0) {
|
||||
throw new ApiError(400, 'roleXid is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
|
||||
throw new ApiError(400, 'permissionMasterXid is required.');
|
||||
}
|
||||
|
||||
if (!activityXids.length) {
|
||||
throw new ApiError(400, 'activityXids is required.');
|
||||
}
|
||||
|
||||
const inviteResult = await hostMemberService.inviteMember({
|
||||
inviterUserXid: userInfo.id,
|
||||
emailAddress,
|
||||
roleXid,
|
||||
permissionMasterXid,
|
||||
activityXids,
|
||||
});
|
||||
|
||||
await sendHostMemberInvitationEmail(
|
||||
inviteResult.user.emailAddress ?? emailAddress,
|
||||
inviteResult.host.companyName,
|
||||
inviteResult.permissionMaster.role.roleName,
|
||||
inviteResult.permissionDetails.map((permission) => permission.displayLabel),
|
||||
inviteResult.activities.map((activity) => activity.activityTitle ?? `Activity #${activity.id}`),
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Host member invited successfully',
|
||||
data: {
|
||||
hostMemberId: inviteResult.hostMember.id,
|
||||
hostXid: inviteResult.hostMember.hostXid,
|
||||
userXid: inviteResult.hostMember.userXid,
|
||||
emailAddress: inviteResult.user.emailAddress,
|
||||
roleXid: inviteResult.hostMember.roleXid,
|
||||
permissionMasterXid: inviteResult.hostMember.hostRolePermissionMasterXid,
|
||||
permissionMasterXids: inviteResult.permissionMaster.permissionMasterXids,
|
||||
permissionLabels: inviteResult.permissionDetails.map((permission) => permission.displayLabel),
|
||||
activityXids: inviteResult.activities.map((activity) => activity.id),
|
||||
activityNames: inviteResult.activities.map((activity) => activity.activityTitle ?? null),
|
||||
memberStatus: inviteResult.hostMember.memberStatus,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
81
src/modules/host/handlers/settings/saveRolePermissions.ts
Normal file
81
src/modules/host/handlers/settings/saveRolePermissions.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostRolePermissionService } from '../../services/hostRolePermission.service';
|
||||
|
||||
const hostRolePermissionService = new HostRolePermissionService(prismaClient);
|
||||
|
||||
interface SaveRolePermissionsBody {
|
||||
roleXid: number;
|
||||
permissionMasterXids: number[];
|
||||
}
|
||||
|
||||
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 verifyHostToken(token);
|
||||
|
||||
let body: Partial<SaveRolePermissionsBody> = {};
|
||||
if (event.body) {
|
||||
try {
|
||||
body = JSON.parse(event.body);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
const roleXid = Number(body.roleXid);
|
||||
const permissionMasterXids = Array.isArray(body.permissionMasterXids)
|
||||
? body.permissionMasterXids
|
||||
: [];
|
||||
|
||||
if (!Number.isInteger(roleXid) || roleXid <= 0) {
|
||||
throw new ApiError(400, 'roleXid is required.');
|
||||
}
|
||||
|
||||
if (!permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'permissionMasterXids is required.');
|
||||
}
|
||||
|
||||
const result = await hostRolePermissionService.saveRolePermissions({
|
||||
hostUserXid: userInfo.id,
|
||||
roleXid,
|
||||
permissionMasterXids,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Role permissions saved successfully',
|
||||
data: {
|
||||
permissionMasterXid: result.saved.id,
|
||||
hostXid: result.saved.hostXid,
|
||||
roleXid: result.saved.roleXid,
|
||||
permissionMasterXids: result.saved.permissionMasterXids,
|
||||
selectedPermissions: result.selectedPermissions,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -533,6 +533,9 @@ export class HostService {
|
||||
id: true,
|
||||
logoPath: true,
|
||||
companyName: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
mobileNumber: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
cities: {
|
||||
@@ -1139,6 +1142,23 @@ export class HostService {
|
||||
},
|
||||
},
|
||||
},
|
||||
ActivitiesMedia: {
|
||||
where: {
|
||||
isActive: true,
|
||||
isCoverImage: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
isCoverImage: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
skip: paginationOptions?.skip || 0,
|
||||
take: paginationOptions?.limit || 10,
|
||||
@@ -1165,11 +1185,31 @@ export class HostService {
|
||||
}
|
||||
}
|
||||
|
||||
const hostActivitiesWithAssets = await Promise.all(
|
||||
hostAllActivities.map(async (activity) => {
|
||||
const coverImage = activity.ActivitiesMedia?.[0] ?? null;
|
||||
const coverImagePresignedUrl = coverImage?.mediaFileName
|
||||
? await getPresignedUrl(
|
||||
bucket,
|
||||
coverImage.mediaFileName.startsWith('http')
|
||||
? coverImage.mediaFileName.split('.com/')[1]
|
||||
: coverImage.mediaFileName,
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...activity,
|
||||
coverImage: coverImage?.mediaFileName ?? null,
|
||||
coverImagePresignedUrl,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
paginationService,
|
||||
} = require('@/common/utils/pagination/pagination.service');
|
||||
return paginationService.createPaginatedResponse(
|
||||
hostAllActivities,
|
||||
hostActivitiesWithAssets,
|
||||
totalCount,
|
||||
paginationOptions || { page: 1, limit: 10, skip: 0 },
|
||||
);
|
||||
@@ -1655,6 +1695,9 @@ export class HostService {
|
||||
data: {
|
||||
host: { connect: { id: createdHost.id } },
|
||||
companyName: parentCompanyData.companyName || null,
|
||||
firstName: parentCompanyData.firstName || null,
|
||||
lastName: parentCompanyData.lastName || null,
|
||||
mobileNumber: parentCompanyData.mobileNumber || null,
|
||||
address1: parentCompanyData.address1 || null,
|
||||
address2: parentCompanyData.address2 || null,
|
||||
// Safely handle city connection - only connect if valid ID exists
|
||||
@@ -1691,7 +1734,7 @@ export class HostService {
|
||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
// parent docs
|
||||
@@ -1871,6 +1914,9 @@ export class HostService {
|
||||
data: {
|
||||
host: { connect: { id: updatedHost.id } },
|
||||
companyName: parentCompanyData.companyName || null,
|
||||
firstName: parentCompanyData.firstName || null,
|
||||
lastName: parentCompanyData.lastName || null,
|
||||
mobileNumber: parentCompanyData.mobileNumber || null,
|
||||
address1: parentCompanyData.address1 || null,
|
||||
address2: parentCompanyData.address2 || null,
|
||||
cities:
|
||||
@@ -1910,7 +1956,7 @@ export class HostService {
|
||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
if (parentDocuments?.length) {
|
||||
@@ -1930,6 +1976,9 @@ export class HostService {
|
||||
where: { id: parentRecord.id },
|
||||
data: {
|
||||
companyName: parentCompanyData.companyName || null,
|
||||
firstName: parentCompanyData.firstName || null,
|
||||
lastName: parentCompanyData.lastName || null,
|
||||
mobileNumber: parentCompanyData.mobileNumber || null,
|
||||
address1: parentCompanyData.address1 || null,
|
||||
address2: parentCompanyData.address2 || null,
|
||||
cities:
|
||||
@@ -1969,7 +2018,7 @@ export class HostService {
|
||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
// if (parentDocuments?.length) {
|
||||
|
||||
497
src/modules/host/services/hostMember.service.ts
Normal file
497
src/modules/host/services/hostMember.service.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
|
||||
import { PaginationOptions } from '../../../common/utils/pagination/pagination.types';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
const ALLOWED_MEMBER_ROLES = new Set([ROLE.CO_ADMIN, ROLE.OPERATOR]);
|
||||
|
||||
function normalizeIdArray(values: unknown): number[] {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isInteger(item) && item > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HostMemberService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async getAllInvitedCoadminAndOperator(input: {
|
||||
hostUserXid: number;
|
||||
search?: string;
|
||||
paginationOptions?: PaginationOptions;
|
||||
}) {
|
||||
const host = await this.prisma.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.hostUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
const filters: any = {
|
||||
hostXid: host.id,
|
||||
roleXid: {
|
||||
in: [ROLE.CO_ADMIN, ROLE.OPERATOR],
|
||||
},
|
||||
memberStatus: 'invited',
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
user: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (input.search?.trim()) {
|
||||
const term = input.search.trim();
|
||||
filters.user = {
|
||||
...filters.user,
|
||||
OR: [
|
||||
{ emailAddress: { contains: term, mode: 'insensitive' as const } },
|
||||
{ firstName: { contains: term, mode: 'insensitive' as const } },
|
||||
{ lastName: { contains: term, mode: 'insensitive' as const } },
|
||||
{ mobileNumber: { contains: term, mode: 'insensitive' as const } },
|
||||
{ userRefNumber: { contains: term, mode: 'insensitive' as const } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const totalCount = await this.prisma.hostMembers.count({
|
||||
where: filters,
|
||||
});
|
||||
|
||||
const members = await this.prisma.hostMembers.findMany({
|
||||
where: filters,
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
memberStatus: true,
|
||||
invitedOn: true,
|
||||
acceptedOn: true,
|
||||
hostRolePermissionMasterXid: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
mobileNumber: true,
|
||||
userRefNumber: true,
|
||||
userStatus: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
invitedBy: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
},
|
||||
},
|
||||
hostRolePermissionMaster: {
|
||||
select: {
|
||||
id: true,
|
||||
permissionMasterXids: true,
|
||||
},
|
||||
},
|
||||
managedActivities: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
activity: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
activityXid: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
invitedOn: 'desc',
|
||||
},
|
||||
skip: input.paginationOptions?.skip ?? 0,
|
||||
take: input.paginationOptions?.limit ?? 10,
|
||||
});
|
||||
|
||||
const permissionIds = Array.from(
|
||||
new Set(
|
||||
members.flatMap((member) =>
|
||||
normalizeIdArray(member.hostRolePermissionMaster?.permissionMasterXids),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const permissionMasters = permissionIds.length
|
||||
? await this.prisma.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: permissionIds },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
displayLabel: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const permissionLabelMap = new Map(
|
||||
permissionMasters.map((permission) => [permission.id, permission.displayLabel]),
|
||||
);
|
||||
|
||||
const data = members.map((member) => {
|
||||
const permissionMasterXids = normalizeIdArray(
|
||||
member.hostRolePermissionMaster?.permissionMasterXids,
|
||||
);
|
||||
|
||||
return {
|
||||
hostMemberId: member.id,
|
||||
hostXid: member.hostXid,
|
||||
hostCompanyName: host.companyName,
|
||||
userXid: member.userXid,
|
||||
roleXid: member.roleXid,
|
||||
roleName: member.role?.roleName ?? member.user.role?.roleName ?? null,
|
||||
permissionMasterXid: member.hostRolePermissionMasterXid,
|
||||
permissionMasterXids,
|
||||
permissionLabels: permissionMasterXids
|
||||
.map((permissionId) => permissionLabelMap.get(permissionId))
|
||||
.filter(Boolean),
|
||||
memberStatus: member.memberStatus,
|
||||
invitedOn: member.invitedOn,
|
||||
acceptedOn: member.acceptedOn,
|
||||
invitedBy: member.invitedBy
|
||||
? {
|
||||
id: member.invitedBy.id,
|
||||
firstName: member.invitedBy.firstName,
|
||||
lastName: member.invitedBy.lastName,
|
||||
emailAddress: member.invitedBy.emailAddress,
|
||||
}
|
||||
: null,
|
||||
user: {
|
||||
id: member.user.id,
|
||||
firstName: member.user.firstName,
|
||||
lastName: member.user.lastName,
|
||||
emailAddress: member.user.emailAddress,
|
||||
mobileNumber: member.user.mobileNumber,
|
||||
userRefNumber: member.user.userRefNumber,
|
||||
userStatus: member.user.userStatus,
|
||||
},
|
||||
activities: member.managedActivities.map((activityLink) => ({
|
||||
id: activityLink.activity.id,
|
||||
activityXid: activityLink.activityXid,
|
||||
activityTitle: activityLink.activity.activityTitle,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async inviteMember(input: {
|
||||
inviterUserXid: number;
|
||||
emailAddress: string;
|
||||
roleXid: number;
|
||||
permissionMasterXid: number;
|
||||
activityXids: unknown;
|
||||
}) {
|
||||
const normalizedEmail = input.emailAddress.trim().toLowerCase();
|
||||
const roleXid = Number(input.roleXid);
|
||||
const permissionMasterXid = Number(input.permissionMasterXid);
|
||||
const activityXids = normalizeIdArray(input.activityXids);
|
||||
|
||||
if (!normalizedEmail) {
|
||||
throw new ApiError(400, 'emailAddress is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(roleXid) || !ALLOWED_MEMBER_ROLES.has(roleXid)) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'roleXid must be one of CO_ADMIN or OPERATOR.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
|
||||
throw new ApiError(400, 'permissionMasterXid is required.');
|
||||
}
|
||||
|
||||
if (!activityXids.length) {
|
||||
throw new ApiError(400, 'At least one activity is required.');
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const host = await tx.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.inviterUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
if (host.userXid !== input.inviterUserXid) {
|
||||
throw new ApiError(403, 'Only the host owner can invite members.');
|
||||
}
|
||||
|
||||
const permissionMaster = await tx.hostRolePermissionMasters.findFirst({
|
||||
where: {
|
||||
id: permissionMasterXid,
|
||||
hostXid: host.id,
|
||||
roleXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
roleXid: true,
|
||||
permissionMasterXids: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!permissionMaster) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
'Permission master not found for the selected host and role.',
|
||||
);
|
||||
}
|
||||
|
||||
const selectedPermissionMasterXids = normalizeIdArray(
|
||||
permissionMaster.permissionMasterXids,
|
||||
);
|
||||
|
||||
const permissionDetails = await tx.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: selectedPermissionMasterXids },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (permissionDetails.length !== selectedPermissionMasterXids.length) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'One or more saved permission XIDs no longer exist in the master table.',
|
||||
);
|
||||
}
|
||||
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: activityXids },
|
||||
hostXid: host.id,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (activities.length !== activityXids.length) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'One or more selected activities are invalid for this host.',
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = await tx.user.findFirst({
|
||||
where: {
|
||||
emailAddress: normalizedEmail,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
let user =
|
||||
existingUser ??
|
||||
(await tx.user.create({
|
||||
data: {
|
||||
emailAddress: normalizedEmail,
|
||||
roleXid: ROLE.HOST,
|
||||
userStatus: USER_STATUS.INVITED,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
}));
|
||||
|
||||
if (existingUser && existingUser.roleXid !== ROLE.HOST) {
|
||||
user = await tx.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: {
|
||||
roleXid: ROLE.HOST,
|
||||
userStatus: USER_STATUS.INVITED,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const existingMembership = await tx.hostMembers.findFirst({
|
||||
where: {
|
||||
userXid: user.id,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMembership && existingMembership.hostXid !== host.id) {
|
||||
throw new ApiError(
|
||||
409,
|
||||
'This person is already invited or assigned to another host company.',
|
||||
);
|
||||
}
|
||||
|
||||
const membershipData = {
|
||||
hostXid: host.id,
|
||||
userXid: user.id,
|
||||
roleXid,
|
||||
hostRolePermissionMasterXid: permissionMaster.id,
|
||||
memberStatus: 'invited',
|
||||
invitedByXid: input.inviterUserXid,
|
||||
invitedOn: new Date(),
|
||||
acceptedOn: null,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const hostMember = existingMembership
|
||||
? await tx.hostMembers.update({
|
||||
where: { id: existingMembership.id },
|
||||
data: membershipData,
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
hostRolePermissionMasterXid: true,
|
||||
memberStatus: true,
|
||||
invitedByXid: true,
|
||||
invitedOn: true,
|
||||
},
|
||||
})
|
||||
: await tx.hostMembers.create({
|
||||
data: membershipData,
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
hostRolePermissionMasterXid: true,
|
||||
memberStatus: true,
|
||||
invitedByXid: true,
|
||||
invitedOn: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.hostMemberActivities.deleteMany({
|
||||
where: {
|
||||
hostMemberXid: hostMember.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.hostMemberActivities.createMany({
|
||||
data: activities.map((activity) => ({
|
||||
hostMemberXid: hostMember.id,
|
||||
activityXid: activity.id,
|
||||
isActive: true,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
user,
|
||||
hostMember,
|
||||
permissionMaster,
|
||||
permissionDetails,
|
||||
activities,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
254
src/modules/host/services/hostRolePermission.service.ts
Normal file
254
src/modules/host/services/hostRolePermission.service.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { ROLE } from '../../../common/utils/constants/common.constant';
|
||||
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
function normalizeIdArray(values: unknown): number[] {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isInteger(item) && item > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HostRolePermissionService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async saveRolePermissions(input: {
|
||||
hostUserXid: number;
|
||||
roleXid: number;
|
||||
permissionMasterXids: unknown;
|
||||
}) {
|
||||
const permissionMasterXids = normalizeIdArray(input.permissionMasterXids);
|
||||
|
||||
if (!permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'permissionMasterXids is required.');
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const host = await tx.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.hostUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
const role = await tx.roles.findFirst({
|
||||
where: {
|
||||
id: input.roleXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new ApiError(400, 'Invalid roleXid.');
|
||||
}
|
||||
|
||||
const selectedPermissions = await tx.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: permissionMasterXids },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (selectedPermissions.length !== permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'One or more permissionMasterXids are invalid.');
|
||||
}
|
||||
|
||||
const saved = await tx.hostRolePermissionMasters.upsert({
|
||||
where: {
|
||||
hostXid_roleXid: {
|
||||
hostXid: host.id,
|
||||
roleXid: role.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
hostXid: host.id,
|
||||
roleXid: role.id,
|
||||
permissionMasterXids,
|
||||
isActive: true,
|
||||
},
|
||||
update: {
|
||||
permissionMasterXids,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
roleXid: true,
|
||||
permissionMasterXids: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
role,
|
||||
saved,
|
||||
selectedPermissions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getMemberPermissions(input: {
|
||||
hostUserXid: number;
|
||||
memberUserXid: number;
|
||||
}) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// Find the host
|
||||
const host = await tx.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.hostUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
// Find the host member
|
||||
const hostMember = await tx.hostMembers.findFirst({
|
||||
where: {
|
||||
hostXid: host.id,
|
||||
userXid: input.memberUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
memberStatus: 'accepted',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
hostRolePermissionMasterXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!hostMember) {
|
||||
throw new ApiError(404, 'Host member not found or not active.');
|
||||
}
|
||||
|
||||
// Check if role is operator or co-admin
|
||||
if (hostMember.roleXid !== ROLE.CO_ADMIN && hostMember.roleXid !== ROLE.OPERATOR) {
|
||||
throw new ApiError(400, 'Member is not an operator or co-admin.');
|
||||
}
|
||||
|
||||
if (!hostMember.hostRolePermissionMasterXid) {
|
||||
// Return empty permissions if no role permissions assigned
|
||||
return {
|
||||
host,
|
||||
member: {
|
||||
userXid: hostMember.userXid,
|
||||
role: hostMember.role,
|
||||
},
|
||||
permissions: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Get the role permissions
|
||||
const rolePermissions = await tx.hostRolePermissionMasters.findFirst({
|
||||
where: {
|
||||
id: hostMember.hostRolePermissionMasterXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionMasterXids: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rolePermissions) {
|
||||
return {
|
||||
host,
|
||||
member: {
|
||||
userXid: hostMember.userXid,
|
||||
role: hostMember.role,
|
||||
},
|
||||
permissions: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Get the actual permissions
|
||||
const permissionIds = rolePermissions.permissionMasterXids as number[];
|
||||
const permissions = await tx.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: permissionIds },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
member: {
|
||||
userXid: hostMember.userXid,
|
||||
role: hostMember.role,
|
||||
},
|
||||
permissions,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
992
src/modules/host/services/operatorActivity.service.ts
Normal file
992
src/modules/host/services/operatorActivity.service.ts
Normal file
@@ -0,0 +1,992 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { brevoService } from '../../../common/email/brevoApi';
|
||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { generateOtpHelper } from '../../../common/utils/helper/sendOtp';
|
||||
import config from '../../../config/config';
|
||||
import {
|
||||
ActivitySummaryDTO,
|
||||
OperatorReservationByCheckInCodeDTO,
|
||||
OperatorReservationVerificationOtpResponseDTO,
|
||||
OperatorReservationVerifyOtpResponseDTO,
|
||||
} from '../dto/operator.activity.dto';
|
||||
|
||||
const formatDateOnly = (date: Date): string => date.toISOString().split('T')[0];
|
||||
|
||||
const formatReadableDateTime = (date: Date): string =>
|
||||
new Intl.DateTimeFormat('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}).format(date);
|
||||
|
||||
const formatReadableDate = (date: Date): string =>
|
||||
new Intl.DateTimeFormat('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
|
||||
const getDateLabel = (date: Date): string => {
|
||||
const today = new Date();
|
||||
return formatDateOnly(today) === formatDateOnly(date)
|
||||
? 'Today'
|
||||
: formatReadableDate(date);
|
||||
};
|
||||
|
||||
const buildFullName = (
|
||||
firstName?: string | null,
|
||||
lastName?: string | null,
|
||||
): string => `${firstName ?? ''} ${lastName ?? ''}`.trim();
|
||||
|
||||
type ReservationVerificationPurpose = 'CheckInVerify' | 'CheckOutVerify';
|
||||
type ReservationVerificationRequestChannel = 'email' | 'mobile';
|
||||
|
||||
@Injectable()
|
||||
export class OperatorActivityService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
private async getAcceptedHostXidsForOperator(
|
||||
operatorId: number,
|
||||
): Promise<number[]> {
|
||||
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||
where: {
|
||||
userXid: operatorId,
|
||||
isActive: true,
|
||||
memberStatus: 'accepted',
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
return hostMembers.map((member) => member.hostXid);
|
||||
}
|
||||
|
||||
private maskEmailAddress(emailAddress: string): string {
|
||||
const [localPart, domainPart = ''] = emailAddress.split('@');
|
||||
|
||||
if (!localPart) {
|
||||
return emailAddress;
|
||||
}
|
||||
|
||||
if (localPart.length <= 2) {
|
||||
return `${localPart[0] ?? '*'}*@${domainPart}`;
|
||||
}
|
||||
|
||||
return `${localPart[0]}${'*'.repeat(
|
||||
Math.max(localPart.length - 2, 1),
|
||||
)}${localPart[localPart.length - 1]}@${domainPart}`;
|
||||
}
|
||||
|
||||
private async sendReservationVerificationOtpEmail(input: {
|
||||
emailAddress: string;
|
||||
otp: string;
|
||||
checkInCode: string;
|
||||
verificationType: 'check-in' | 'checkout';
|
||||
firstName?: string | null;
|
||||
}): Promise<void> {
|
||||
const subject =
|
||||
input.verificationType === 'check-in'
|
||||
? 'Your Minglar check-in verification code'
|
||||
: 'Your Minglar checkout verification code';
|
||||
|
||||
const greetingName = input.firstName?.trim() || 'there';
|
||||
const htmlContent = `
|
||||
<p>Hi ${greetingName},</p>
|
||||
<p>Your ${input.verificationType} verification code for Minglar is:</p>
|
||||
<p><strong>${input.otp}</strong></p>
|
||||
<p>Check-in code: <strong>${input.checkInCode}</strong></p>
|
||||
<p>This code is valid for the next 5 minutes.</p>
|
||||
<p>If you did not request this code, you can ignore this email.</p>
|
||||
<p>Warm regards,<br />Team Minglar</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
await brevoService.sendEmail({
|
||||
recipients: [{ email: input.emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Reservation OTP email send failed:', error);
|
||||
throw new ApiError(500, 'Failed to send OTP to the registered email address.');
|
||||
}
|
||||
}
|
||||
|
||||
private async findReservationForOperatorByCheckInCode(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
) {
|
||||
const hostXids = await this.getAcceptedHostXidsForOperator(operatorId);
|
||||
|
||||
if (hostXids.length === 0) {
|
||||
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||
}
|
||||
|
||||
const reservation = await this.prisma.itineraryDetails.findFirst({
|
||||
where: {
|
||||
offlineCode: checkInCode,
|
||||
itineraryKind: 'ACTIVITY',
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
itineraryActivity: {
|
||||
itineraryType: 'ACTIVITY',
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
activity: {
|
||||
hostXid: {
|
||||
in: hostXids,
|
||||
},
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
itineraryMemberXid: true,
|
||||
offlineCode: true,
|
||||
activityStatus: true,
|
||||
itineraryMember: {
|
||||
select: {
|
||||
id: true,
|
||||
member: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
mobileNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
itineraryActivity: {
|
||||
select: {
|
||||
id: true,
|
||||
bookingStatus: true,
|
||||
activity: {
|
||||
select: {
|
||||
activityTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!reservation) {
|
||||
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||
}
|
||||
|
||||
return reservation;
|
||||
}
|
||||
|
||||
private async verifyReservationOtp(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
otp: string,
|
||||
input: {
|
||||
otpType: ReservationVerificationPurpose;
|
||||
verificationType: 'check-in' | 'checkout';
|
||||
nextStatus: 'checked_in' | 'checked_out';
|
||||
},
|
||||
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||
const normalizedCheckInCode = checkInCode.trim();
|
||||
const trimmedOtp = otp.trim();
|
||||
|
||||
if (!normalizedCheckInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required');
|
||||
}
|
||||
|
||||
if (!trimmedOtp) {
|
||||
throw new ApiError(400, 'otp is required');
|
||||
}
|
||||
|
||||
const reservation = await this.findReservationForOperatorByCheckInCode(
|
||||
operatorId,
|
||||
normalizedCheckInCode,
|
||||
);
|
||||
|
||||
const member = reservation.itineraryMember?.member;
|
||||
if (!member?.id) {
|
||||
throw new ApiError(404, 'User not found for this reservation');
|
||||
}
|
||||
|
||||
const currentStatus = (
|
||||
reservation.activityStatus ??
|
||||
reservation.itineraryActivity.bookingStatus ??
|
||||
''
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (input.verificationType === 'check-in') {
|
||||
if (currentStatus === 'checked_in') {
|
||||
throw new ApiError(400, 'This user is already checked in.');
|
||||
}
|
||||
|
||||
if (currentStatus === 'checked_out') {
|
||||
throw new ApiError(400, 'This user has already checked out.');
|
||||
}
|
||||
}
|
||||
|
||||
if (input.verificationType === 'checkout') {
|
||||
if (currentStatus === 'checked_out') {
|
||||
throw new ApiError(400, 'This user is already checked out.');
|
||||
}
|
||||
|
||||
if (currentStatus !== 'checked_in') {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Check-in verification must be completed before checkout.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const latestOtp = await this.prisma.userOtp.findFirst({
|
||||
where: {
|
||||
userXid: member.id,
|
||||
otpType: input.otpType,
|
||||
isActive: true,
|
||||
isVerified: false,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!latestOtp) {
|
||||
throw new ApiError(400, 'No OTP found.');
|
||||
}
|
||||
|
||||
if (new Date() > latestOtp.expiresOn) {
|
||||
throw new ApiError(400, 'OTP has expired.');
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(trimmedOtp, latestOtp.otpCode);
|
||||
|
||||
if (!isMatch) {
|
||||
throw new ApiError(400, 'Invalid OTP.');
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.userOtp.update({
|
||||
where: {
|
||||
id: latestOtp.id,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
verifiedOn: new Date(),
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.itineraryDetails.update({
|
||||
where: {
|
||||
id: reservation.id,
|
||||
},
|
||||
data: {
|
||||
activityStatus: input.nextStatus,
|
||||
updatedOn: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||
itineraryMemberXid: reservation.itineraryMemberXid,
|
||||
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
|
||||
checkInCode: normalizedCheckInCode,
|
||||
verificationType: input.verificationType,
|
||||
verified: true,
|
||||
reservationStatus: input.nextStatus,
|
||||
};
|
||||
}
|
||||
|
||||
private async requestReservationVerificationOtp(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
input: {
|
||||
otpType: ReservationVerificationPurpose;
|
||||
verificationType: 'check-in' | 'checkout';
|
||||
requestedChannel: ReservationVerificationRequestChannel;
|
||||
},
|
||||
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||
const normalizedCheckInCode = checkInCode.trim();
|
||||
|
||||
if (!normalizedCheckInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required');
|
||||
}
|
||||
|
||||
const reservation = await this.findReservationForOperatorByCheckInCode(
|
||||
operatorId,
|
||||
normalizedCheckInCode,
|
||||
);
|
||||
|
||||
const member = reservation.itineraryMember?.member;
|
||||
if (!member?.id) {
|
||||
throw new ApiError(404, 'User not found for this reservation');
|
||||
}
|
||||
|
||||
const emailAddress = member.emailAddress?.trim().toLowerCase() || null;
|
||||
const mobileNumber = member.mobileNumber?.trim() || null;
|
||||
|
||||
if (!emailAddress && !mobileNumber) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'No registered mobile number or email address found for this user.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!emailAddress) {
|
||||
throw new ApiError(
|
||||
501,
|
||||
'SMS OTP delivery is not configured yet for reservation verification. Please add a registered email address for this user.',
|
||||
);
|
||||
}
|
||||
|
||||
const deliveryNote =
|
||||
input.requestedChannel === 'mobile'
|
||||
? 'SMS OTP delivery is not configured yet, so the OTP was sent to the registered email address instead.'
|
||||
: null;
|
||||
|
||||
const otpResult = await generateOtpHelper(
|
||||
this.prisma,
|
||||
member.id,
|
||||
emailAddress,
|
||||
input.otpType,
|
||||
6,
|
||||
5,
|
||||
);
|
||||
|
||||
await this.sendReservationVerificationOtpEmail({
|
||||
emailAddress,
|
||||
otp: otpResult.otp,
|
||||
checkInCode: normalizedCheckInCode,
|
||||
verificationType: input.verificationType,
|
||||
firstName: member.firstName,
|
||||
});
|
||||
|
||||
return {
|
||||
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||
itineraryMemberXid: reservation.itineraryMemberXid,
|
||||
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
|
||||
checkInCode: normalizedCheckInCode,
|
||||
verificationType: input.verificationType,
|
||||
requestedChannel: input.requestedChannel,
|
||||
deliveryChannel: 'email',
|
||||
destination: this.maskEmailAddress(emailAddress),
|
||||
reservationStatus:
|
||||
reservation.activityStatus ?? reservation.itineraryActivity.bookingStatus,
|
||||
expiresInMinutes: 5,
|
||||
deliveryNote,
|
||||
};
|
||||
}
|
||||
|
||||
private async attachPresignedUrl(
|
||||
fileName?: string | null,
|
||||
): Promise<string | null> {
|
||||
if (!fileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await getPresignedUrl(config.aws.bucketName, fileName);
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate presigned URL for ${fileName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getActivitiesByDate(
|
||||
operatorId: number,
|
||||
activityDate?: string,
|
||||
): Promise<{
|
||||
date: string;
|
||||
activities: ActivitySummaryDTO[];
|
||||
totalCount: number;
|
||||
}> {
|
||||
try {
|
||||
// Get operator's assigned hosts
|
||||
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||
where: {
|
||||
userXid: operatorId,
|
||||
isActive: true,
|
||||
memberStatus: 'accepted', // Only accepted memberships
|
||||
},
|
||||
select: {
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (hostMembers.length === 0) {
|
||||
return {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
activities: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const hostXids = hostMembers.map((m) => m.hostXid);
|
||||
|
||||
// Use today's date if not provided
|
||||
const queryDate = activityDate ? new Date(activityDate) : new Date();
|
||||
|
||||
// Validate date format
|
||||
if (isNaN(queryDate.getTime())) {
|
||||
throw new ApiError(400, 'Invalid date format. Use YYYY-MM-DD format');
|
||||
}
|
||||
|
||||
// Set time to start of day (UTC)
|
||||
queryDate.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
// Set end of day
|
||||
const endOfDay = new Date(queryDate);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
// Get all schedule occurrences from today onwards (not just query date)
|
||||
// This includes future dates to show all scheduled dates
|
||||
const allScheduleOccurrences =
|
||||
await this.prisma.scheduleOccurences.findMany({
|
||||
where: {
|
||||
occurenceDate: {
|
||||
gte: queryDate,
|
||||
},
|
||||
isActive: true,
|
||||
scheduleHeader: {
|
||||
activity: {
|
||||
hostXid: {
|
||||
in: hostXids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
scheduleHeader: {
|
||||
include: {
|
||||
activity: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
isActive: true,
|
||||
ActivitiesMedia: {
|
||||
where: {
|
||||
isCoverImage: true,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
mediaFileName: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
occurenceDate: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (allScheduleOccurrences.length === 0) {
|
||||
return {
|
||||
date: queryDate.toISOString().split('T')[0],
|
||||
activities: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Group activities by activity ID with all scheduled dates
|
||||
const activityMap = new Map<
|
||||
number,
|
||||
{
|
||||
activityTitle: string;
|
||||
coverImage: string | null;
|
||||
coverImageUrl: string | null;
|
||||
activityId: number;
|
||||
scheduledDates: Date[];
|
||||
}
|
||||
>();
|
||||
|
||||
// Process all schedule occurrences to build activity list with all scheduled dates
|
||||
for (const occurrence of allScheduleOccurrences) {
|
||||
const activity = occurrence.scheduleHeader.activity;
|
||||
|
||||
if (!activityMap.has(activity.id)) {
|
||||
let coverImage: string | null = null;
|
||||
let coverImageUrl: string | null = null;
|
||||
|
||||
if (activity.ActivitiesMedia.length > 0) {
|
||||
coverImage = activity.ActivitiesMedia[0].mediaFileName;
|
||||
try {
|
||||
// Generate presigned URL for the image
|
||||
coverImageUrl = await getPresignedUrl(
|
||||
config.aws.bucketName,
|
||||
coverImage,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to generate presigned URL for ${coverImage}:`,
|
||||
error,
|
||||
);
|
||||
coverImageUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
activityMap.set(activity.id, {
|
||||
activityTitle: activity.activityTitle || 'Unknown Activity',
|
||||
coverImage,
|
||||
coverImageUrl,
|
||||
activityId: activity.id,
|
||||
scheduledDates: [new Date(occurrence.occurenceDate)],
|
||||
});
|
||||
} else {
|
||||
// Add to scheduled dates if not already present
|
||||
const existingActivity = activityMap.get(activity.id)!;
|
||||
const occurrenceDateStr = new Date(occurrence.occurenceDate)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
const dateExists = existingActivity.scheduledDates.some(
|
||||
(d) => d.toISOString().split('T')[0] === occurrenceDateStr,
|
||||
);
|
||||
if (!dateExists) {
|
||||
existingActivity.scheduledDates.push(
|
||||
new Date(occurrence.occurenceDate),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all unique scheduled dates for all activities
|
||||
const allScheduledDates = new Set<string>();
|
||||
activityMap.forEach((activity) => {
|
||||
activity.scheduledDates.forEach((date) => {
|
||||
allScheduledDates.add(date.toISOString().split('T')[0]);
|
||||
});
|
||||
});
|
||||
|
||||
// Get all activity IDs
|
||||
const activityIds = Array.from(activityMap.keys());
|
||||
|
||||
// Query all bookings for these activities from query date onwards
|
||||
const allBookings = await this.prisma.itineraryActivities.findMany({
|
||||
where: {
|
||||
activityXid: {
|
||||
in: activityIds,
|
||||
},
|
||||
occurenceDate: {
|
||||
gte: queryDate,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
itineraryHeaderXid: true,
|
||||
occurenceDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Count bookings by activity and date
|
||||
const bookingsByActivityAndDate = new Map<
|
||||
number,
|
||||
Map<string, Set<number>>
|
||||
>();
|
||||
|
||||
allBookings.forEach((booking) => {
|
||||
if (booking.activityXid) {
|
||||
if (!bookingsByActivityAndDate.has(booking.activityXid)) {
|
||||
bookingsByActivityAndDate.set(booking.activityXid, new Map());
|
||||
}
|
||||
|
||||
const dateStr = new Date(booking.occurenceDate)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
const dateMap = bookingsByActivityAndDate.get(booking.activityXid)!;
|
||||
|
||||
if (!dateMap.has(dateStr)) {
|
||||
dateMap.set(dateStr, new Set());
|
||||
}
|
||||
|
||||
dateMap.get(dateStr)!.add(booking.itineraryHeaderXid);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map to array format with date breakdown
|
||||
const activities: ActivitySummaryDTO[] = Array.from(
|
||||
activityMap.values(),
|
||||
).map((activity) => {
|
||||
const activityBookings = bookingsByActivityAndDate.get(
|
||||
activity.activityId,
|
||||
);
|
||||
|
||||
// Get bookings for each scheduled date
|
||||
const dateBreakdown = Array.from(allScheduledDates)
|
||||
.sort()
|
||||
.map((dateStr) => {
|
||||
const count = activityBookings?.get(dateStr)?.size || 0;
|
||||
return {
|
||||
date: dateStr,
|
||||
count,
|
||||
};
|
||||
});
|
||||
|
||||
// Count for the query date only
|
||||
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||
const countForQueryDate =
|
||||
activityBookings?.get(queryDateStr)?.size || 0;
|
||||
|
||||
return {
|
||||
activityName: activity.activityTitle,
|
||||
activityImage: activity.coverImage,
|
||||
activityImagePreSignedUrl: activity.coverImageUrl,
|
||||
count: countForQueryDate,
|
||||
dateBreakdown,
|
||||
};
|
||||
});
|
||||
|
||||
// Total count is bookings for the requested date only
|
||||
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||
const totalCount = allBookings.filter(
|
||||
(b) =>
|
||||
new Date(b.occurenceDate).toISOString().split('T')[0] ===
|
||||
queryDateStr,
|
||||
).length;
|
||||
|
||||
return {
|
||||
date: queryDateStr,
|
||||
activities,
|
||||
totalCount,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
500,
|
||||
error instanceof Error ? error.message : 'Error fetching activities',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getReservationByCheckInCode(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
): Promise<OperatorReservationByCheckInCodeDTO> {
|
||||
try {
|
||||
const normalizedCheckInCode = checkInCode.trim();
|
||||
if (!normalizedCheckInCode) {
|
||||
throw new ApiError(400, 'checkInCode is required');
|
||||
}
|
||||
|
||||
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||
where: {
|
||||
userXid: operatorId,
|
||||
isActive: true,
|
||||
memberStatus: 'accepted',
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const hostXids = hostMembers.map((member) => member.hostXid);
|
||||
if (hostXids.length === 0) {
|
||||
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||
}
|
||||
|
||||
const reservation = await this.prisma.itineraryDetails.findFirst({
|
||||
where: {
|
||||
offlineCode: normalizedCheckInCode,
|
||||
itineraryKind: 'ACTIVITY',
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
itineraryActivity: {
|
||||
itineraryType: 'ACTIVITY',
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
activity: {
|
||||
hostXid: {
|
||||
in: hostXids,
|
||||
},
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
itineraryMemberXid: true,
|
||||
offlineCode: true,
|
||||
activityStatus: true,
|
||||
createdAt: true,
|
||||
paidOn: true,
|
||||
itineraryMember: {
|
||||
select: {
|
||||
id: true,
|
||||
memberRole: true,
|
||||
member: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
mobileNumber: true,
|
||||
profileImage: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
itineraryActivity: {
|
||||
select: {
|
||||
id: true,
|
||||
occurenceDate: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
bookingStatus: true,
|
||||
venue: {
|
||||
select: {
|
||||
id: true,
|
||||
venueName: true,
|
||||
venueLabel: true,
|
||||
},
|
||||
},
|
||||
itineraryHeader: {
|
||||
select: {
|
||||
id: true,
|
||||
itineraryNo: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
activity: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
checkInAddress: true,
|
||||
ActivityPickUpDetails: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isPickUp: true,
|
||||
locationAddress: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
itineraryActivitySelections: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
itineraryMemberXid: true,
|
||||
isFoodOpted: true,
|
||||
isTrainerOpted: true,
|
||||
selectedFoodTypes: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityFoodType: {
|
||||
select: {
|
||||
foodType: {
|
||||
select: {
|
||||
foodTypeName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
selectedEquipments: {
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityEquipment: {
|
||||
select: {
|
||||
equipmentName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!reservation) {
|
||||
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||
}
|
||||
|
||||
const activitySelection =
|
||||
reservation.itineraryActivity.itineraryActivitySelections.find(
|
||||
(selection) =>
|
||||
selection.itineraryMemberXid === reservation.itineraryMemberXid,
|
||||
) ?? null;
|
||||
|
||||
const selectedFoodTypes = (activitySelection?.selectedFoodTypes ?? [])
|
||||
.map(
|
||||
(selectedFoodType) =>
|
||||
selectedFoodType.activityFoodType.foodType.foodTypeName,
|
||||
)
|
||||
.filter(Boolean);
|
||||
const selectedEquipments = (activitySelection?.selectedEquipments ?? [])
|
||||
.map(
|
||||
(selectedEquipment) =>
|
||||
selectedEquipment.activityEquipment.equipmentName,
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const pickupLocation =
|
||||
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
|
||||
(detail) => detail.isPickUp && detail.locationAddress,
|
||||
)?.locationAddress ??
|
||||
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
|
||||
(detail) => detail.locationAddress,
|
||||
)?.locationAddress ??
|
||||
reservation.itineraryActivity.activity.checkInAddress ??
|
||||
null;
|
||||
|
||||
const member = reservation.itineraryMember.member;
|
||||
const fullName =
|
||||
buildFullName(member.firstName, member.lastName) || 'Guest';
|
||||
const profileImagePreSignedUrl = await this.attachPresignedUrl(
|
||||
member.profileImage,
|
||||
);
|
||||
const occurenceDate = new Date(
|
||||
reservation.itineraryActivity.occurenceDate,
|
||||
);
|
||||
const bookedOnDate =
|
||||
reservation.paidOn ??
|
||||
reservation.createdAt ??
|
||||
reservation.itineraryActivity.itineraryHeader.createdAt;
|
||||
|
||||
return {
|
||||
itineraryHeaderXid: reservation.itineraryActivity.itineraryHeader.id,
|
||||
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||
bookingId: reservation.itineraryActivity.itineraryHeader.itineraryNo,
|
||||
checkInCode: reservation.offlineCode,
|
||||
reservationStatus:
|
||||
reservation.activityStatus ??
|
||||
reservation.itineraryActivity.bookingStatus,
|
||||
personalDetails: {
|
||||
fullName,
|
||||
firstName: member.firstName,
|
||||
lastName: member.lastName,
|
||||
role: reservation.itineraryMember.memberRole,
|
||||
mobileNumber: member.mobileNumber,
|
||||
profileImage: member.profileImage,
|
||||
profileImagePreSignedUrl,
|
||||
tags: [],
|
||||
},
|
||||
bookingInformation: {
|
||||
activityName: reservation.itineraryActivity.activity.activityTitle,
|
||||
slot:
|
||||
reservation.itineraryActivity.startTime &&
|
||||
reservation.itineraryActivity.endTime
|
||||
? `${reservation.itineraryActivity.startTime} - ${reservation.itineraryActivity.endTime}`
|
||||
: null,
|
||||
startTime: reservation.itineraryActivity.startTime,
|
||||
endTime: reservation.itineraryActivity.endTime,
|
||||
track: reservation.itineraryActivity.venue?.venueName ?? null,
|
||||
trackLabel: reservation.itineraryActivity.venue?.venueLabel ?? null,
|
||||
date: formatDateOnly(occurenceDate),
|
||||
dateLabel: getDateLabel(occurenceDate),
|
||||
bookedOn: bookedOnDate ? bookedOnDate.toISOString() : null,
|
||||
bookedOnLabel: bookedOnDate
|
||||
? formatReadableDateTime(bookedOnDate)
|
||||
: null,
|
||||
},
|
||||
bookingIncluded: {
|
||||
food:
|
||||
selectedFoodTypes.length > 0 ? selectedFoodTypes.join(', ') : 'No',
|
||||
selectedFoodTypes,
|
||||
equipment: selectedEquipments.length > 0 ? 'Yes' : 'No',
|
||||
selectedEquipments,
|
||||
trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No',
|
||||
pickupLocation,
|
||||
},
|
||||
// description1: null,
|
||||
// description2: null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
500,
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Error fetching reservation details',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async sendOtpCheckIn(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
|
||||
otpType: 'CheckInVerify',
|
||||
verificationType: 'check-in',
|
||||
requestedChannel: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
async sendOtpCheckout(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
|
||||
otpType: 'CheckOutVerify',
|
||||
verificationType: 'checkout',
|
||||
requestedChannel: 'mobile',
|
||||
});
|
||||
}
|
||||
|
||||
async verifyOtpCheckIn(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
otp: string,
|
||||
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
|
||||
otpType: 'CheckInVerify',
|
||||
verificationType: 'check-in',
|
||||
nextStatus: 'checked_in',
|
||||
});
|
||||
}
|
||||
|
||||
async verifyOtpCheckout(
|
||||
operatorId: number,
|
||||
checkInCode: string,
|
||||
otp: string,
|
||||
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
|
||||
otpType: 'CheckOutVerify',
|
||||
verificationType: 'checkout',
|
||||
nextStatus: 'checked_out',
|
||||
});
|
||||
}
|
||||
}
|
||||
286
src/modules/host/services/operatorAuth.service.ts
Normal file
286
src/modules/host/services/operatorAuth.service.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator';
|
||||
|
||||
type OperatorSignupInput = {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
emailAddress?: string;
|
||||
isdCode?: string;
|
||||
mobileNumber?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OperatorAuthService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async findInvitedOperator(input: {
|
||||
emailAddress?: string;
|
||||
mobileNumber?: string;
|
||||
}) {
|
||||
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||
const mobileNumber = input.mobileNumber?.trim();
|
||||
|
||||
if (!emailAddress && !mobileNumber) {
|
||||
throw new ApiError(400, 'Email address or mobile number is required');
|
||||
}
|
||||
|
||||
const invitedOperator = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
roleXid: ROLE.OPERATOR,
|
||||
isActive: true,
|
||||
OR: [
|
||||
...(emailAddress ? [{ emailAddress }] : []),
|
||||
...(mobileNumber ? [{ mobileNumber }] : []),
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
isdCode: true,
|
||||
mobileNumber: true,
|
||||
roleXid: true,
|
||||
userPassword: true,
|
||||
isEmailVerfied: true,
|
||||
userStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invitedOperator) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
'Operator record not found. Please contact your host.',
|
||||
);
|
||||
}
|
||||
|
||||
return invitedOperator;
|
||||
}
|
||||
|
||||
async signUpOperator(input: OperatorSignupInput) {
|
||||
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||
const mobileNumber = input.mobileNumber?.trim();
|
||||
const invitedOperator = await this.findInvitedOperator({
|
||||
emailAddress,
|
||||
mobileNumber,
|
||||
});
|
||||
|
||||
if (invitedOperator.userPassword) {
|
||||
return {
|
||||
userId: invitedOperator.id,
|
||||
emailAddress: emailAddress || invitedOperator.emailAddress,
|
||||
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
|
||||
otp: null,
|
||||
expiresOn: null,
|
||||
isNewOperator: false,
|
||||
};
|
||||
}
|
||||
|
||||
const otp = OtpGeneratorSixDigit.generateOtp();
|
||||
const hashedOtp = await bcrypt.hash(otp, 10);
|
||||
const expiry = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: { id: invitedOperator.id },
|
||||
data: {
|
||||
firstName: input.firstName?.trim() || invitedOperator.firstName || null,
|
||||
lastName: input.lastName?.trim() || invitedOperator.lastName || null,
|
||||
emailAddress: emailAddress || invitedOperator.emailAddress,
|
||||
isdCode: input.isdCode?.trim() || invitedOperator.isdCode || null,
|
||||
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
|
||||
userStatus: USER_STATUS.INVITED,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.userOtp.updateMany({
|
||||
where: {
|
||||
userXid: invitedOperator.id,
|
||||
otpType: 'Register',
|
||||
isActive: true,
|
||||
},
|
||||
data: {
|
||||
isActive: false,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.userOtp.create({
|
||||
data: {
|
||||
userXid: invitedOperator.id,
|
||||
otpType: 'Register',
|
||||
otpCode: hashedOtp,
|
||||
expiresOn: expiry,
|
||||
isVerified: false,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isNewOperator: true,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyOperatorOtp(input: { emailAddress?: string; mobileNumber?: string; otp: string }) {
|
||||
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||
const mobileNumber = input.mobileNumber?.trim();
|
||||
const otp = input.otp.trim();
|
||||
|
||||
const invitedOperator = await this.findInvitedOperator({
|
||||
emailAddress,
|
||||
mobileNumber,
|
||||
});
|
||||
|
||||
const latestOtp = await this.prisma.userOtp.findFirst({
|
||||
where: {
|
||||
userXid: invitedOperator.id,
|
||||
otpType: 'Register',
|
||||
isActive: true,
|
||||
isVerified: false,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!latestOtp) {
|
||||
throw new ApiError(400, 'No OTP found.');
|
||||
}
|
||||
|
||||
if (new Date() > latestOtp.expiresOn) {
|
||||
throw new ApiError(400, 'OTP has expired.');
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(otp, latestOtp.otpCode);
|
||||
|
||||
if (!isMatch) {
|
||||
throw new ApiError(400, 'Invalid OTP.');
|
||||
}
|
||||
|
||||
await this.prisma.userOtp.update({
|
||||
where: { id: latestOtp.id },
|
||||
data: {
|
||||
isVerified: true,
|
||||
verifiedOn: new Date(),
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: invitedOperator.id },
|
||||
data: {
|
||||
isEmailVerfied: emailAddress ? true : invitedOperator.isEmailVerfied,
|
||||
},
|
||||
});
|
||||
|
||||
return invitedOperator;
|
||||
}
|
||||
|
||||
async createOperatorPassword(userId: number, password: string) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
roleXid: ROLE.OPERATOR,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
userPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(404, 'Operator not found');
|
||||
}
|
||||
|
||||
if (user.userPassword) {
|
||||
throw new ApiError(400, 'Password already created. Please login.');
|
||||
}
|
||||
|
||||
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
userPassword: hashedPassword,
|
||||
userStatus: USER_STATUS.ACTIVE,
|
||||
isEmailVerfied: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
emailAddress: user.emailAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async loginForOperator(emailAddress: string, userPassword: string) {
|
||||
const existingOperator = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
emailAddress,
|
||||
roleXid: ROLE.OPERATOR,
|
||||
isActive: true,
|
||||
userStatus: USER_STATUS.ACTIVE,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
mobileNumber: true,
|
||||
roleXid: true,
|
||||
isActive: true,
|
||||
userStatus: true,
|
||||
userPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOperator) {
|
||||
throw new ApiError(404, 'Operator not found');
|
||||
}
|
||||
|
||||
const isPasswordMatched = await bcrypt.compare(
|
||||
userPassword,
|
||||
existingOperator.userPassword || '',
|
||||
);
|
||||
|
||||
if (!isPasswordMatched) {
|
||||
throw new ApiError(401, 'Invalid credentials');
|
||||
}
|
||||
|
||||
delete existingOperator.userPassword;
|
||||
|
||||
return existingOperator;
|
||||
}
|
||||
|
||||
async verifyPasswordForOperator(emailAddress: string, userPassword: string) {
|
||||
const existingOperator = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
emailAddress,
|
||||
roleXid: ROLE.OPERATOR,
|
||||
isActive: true,
|
||||
userStatus: USER_STATUS.ACTIVE,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOperator) {
|
||||
throw new ApiError(404, 'Operator not found');
|
||||
}
|
||||
|
||||
const isPasswordMatched = await bcrypt.compare(
|
||||
userPassword,
|
||||
existingOperator.userPassword || '',
|
||||
);
|
||||
|
||||
return isPasswordMatched;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { brevoService } from '../../../common/email/brevoApi';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
export async function sendHostMemberInvitationEmail(
|
||||
emailAddress: string,
|
||||
hostName: string,
|
||||
memberRole: string,
|
||||
permissionLabels: string[],
|
||||
activityNames: string[],
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
}> {
|
||||
const subject = `Invitation to join ${hostName} on Minglar Host`;
|
||||
|
||||
const permissionsHtml = permissionLabels.length
|
||||
? `<ul>${permissionLabels.map((permission) => `<li>${permission}</li>`).join('')}</ul>`
|
||||
: '<p>No permissions were assigned.</p>';
|
||||
|
||||
const activitiesHtml = activityNames.length
|
||||
? `<ul>${activityNames.map((activity) => `<li>${activity}</li>`).join('')}</ul>`
|
||||
: '<p>No activities were assigned.</p>';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi there,</p>
|
||||
<p>You have been invited by <strong>${hostName}</strong> to join the Minglar Host portal as <strong>${memberRole}</strong>.</p>
|
||||
<p>The following permissions have been assigned to your account:</p>
|
||||
${permissionsHtml}
|
||||
<p>The following activities have been assigned to you:</p>
|
||||
${activitiesHtml}
|
||||
<p>You can access the host portal using the link below:</p>
|
||||
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p>
|
||||
<p>If you were not expecting this invitation, you can ignore this email.</p>
|
||||
<p>Warm regards,<br/>Team Minglar</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send host member invitation email.');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { brevoService } from "@/common/email/brevoApi";
|
||||
import ApiError from "@/common/utils/helper/ApiError";
|
||||
import config from '../../../config/config';
|
||||
|
||||
export async function sendEmailToAM(
|
||||
emailAddress: string,
|
||||
amName: string,
|
||||
hostCompanyName: string,
|
||||
hostRefNumber: string
|
||||
activityName: string
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
@@ -13,13 +14,27 @@ export async function sendEmailToAM(
|
||||
|
||||
const subject = `${hostCompanyName} Has Resubmitted Their Application`;
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hello ${amName},</p>
|
||||
<p>${hostCompanyName} has updated and re-submitted their application for your review.</p>
|
||||
<p>Reference number: <strong>${hostRefNumber}</strong></p>
|
||||
<p>Please log in to your dashboard to review the revised submission and proceed with the necessary action.</p>
|
||||
<p>Thank you,<br/>Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hello ${amName},</p>
|
||||
|
||||
<p>
|
||||
${hostCompanyName} has updated and re-submitted their pre-qualification details of ${activityName} for your review.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please click on the link below to log in to your dashboard to review the revised submission and proceed with the necessary action.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Review Application</strong><br/>
|
||||
<a href="${config.AM_INTERFACE_LINK}" target="_blank">${config.AM_INTERFACE_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thank you,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
@@ -83,7 +98,7 @@ export async function sendPQPEmailToAM(
|
||||
emailAddress: string,
|
||||
minglarAdminName: string,
|
||||
hostCompanyName: string,
|
||||
hostRefNumber: string
|
||||
activityName: string
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
@@ -91,13 +106,27 @@ export async function sendPQPEmailToAM(
|
||||
|
||||
const subject = "New Host Application Submitted for Review";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${minglarAdminName},</p>
|
||||
<p>${hostCompanyName} has submitted the pre-qualification details and is awaiting your review.</p>
|
||||
<p>Reference number: <strong>${hostRefNumber}</strong></p>
|
||||
<p>Please log in to your dashboard to review the submission and take the necessary action.</p>
|
||||
<p>Thank you,<br/>Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${minglarAdminName},</p>
|
||||
|
||||
<p>
|
||||
${hostCompanyName} has submitted the pre-qualification details for ${activityName} and is awaiting your review.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please click the link below to log in to your dashboard, review the submission, and take the necessary action:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Review Application</strong><br/>
|
||||
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thank you,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { brevoService } from "../../../common/email/brevoApi";
|
||||
import ApiError from "../../../common/utils/helper/ApiError";
|
||||
import config from "../../../config/config";
|
||||
import { brevoService } from '../../../common/email/brevoApi';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
export async function sendOtpEmailForHost(
|
||||
emailAddress: string,
|
||||
otp: string | number
|
||||
otp: string | number,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
|
||||
const subject = "Your Minglar Verification Code";
|
||||
const subject = 'Your Minglar Verification Code';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi there,</p>
|
||||
<p>Here's your verification code to get started:</p>
|
||||
<p>Hi there 👋</p>
|
||||
<p>Here’s 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 didn't request this, you can safely ignore this email.</p>
|
||||
<p>Once verified, you can continue setting up your Minglar account. If you didn’t 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/>Team Minglar</p>
|
||||
<p>Warm regards,<br />Team Minglar</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
@@ -36,36 +35,51 @@ export async function sendOtpEmailForHost(
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to host via email.");
|
||||
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<{
|
||||
export async function sendWelcomeEmailToHost(emailAddress: string): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
|
||||
const subject = "Get Started as a Minglar Host";
|
||||
const subject = 'Get Started as a Minglar Host';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${emailAddress},</p>
|
||||
<p>We're excited to have you join Minglar as a host. Welcome aboard!</p>
|
||||
<p>To get started and bring your activities live, here's what comes next:</p>
|
||||
<p><strong>Your next steps:</strong></p>
|
||||
<p>1. Complete your host profile</p>
|
||||
<p>2. Complete the pre-qualification process for all your activities</p>
|
||||
<p>3. Submit your activity details for review</p>
|
||||
<p>4. Go live and start receiving bookings</p>
|
||||
<p><strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
<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>
|
||||
<p>We're looking forward to seeing your experiences come to life on Minglar.</p>
|
||||
<p>Warm regards,<br/>Team Minglar</p>
|
||||
`;
|
||||
<p>Hi ${emailAddress},</p>
|
||||
|
||||
<p>We’re excited to have you join Minglar as a host. Welcome aboard! 🌟</p>
|
||||
|
||||
<p>To get started and bring your activities live, here’s what comes next:</p>
|
||||
|
||||
<p><strong>Your next steps:</strong></p>
|
||||
|
||||
<p>1. Complete your host profile</p>
|
||||
<p>2. Complete the pre-qualification process for all your activities</p>
|
||||
<p>3. Submit your activity details for review</p>
|
||||
<p>4. Go live and start receiving bookings</p>
|
||||
|
||||
<p>
|
||||
👉 <strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you need any support along the way, our team is always here to help.
|
||||
You can reach us anytime at
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We’re looking forward to seeing your experiences come to life on Minglar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Team Minglar
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
@@ -81,8 +95,7 @@ export async function sendWelcomeEmailToHost(
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to host via email.");
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send OTP to host via email.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
src/modules/minglaradmin/handlers/addPQQSuggestion.ts
Normal file
91
src/modules/minglaradmin/handlers/addPQQSuggestion.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { MinglarService } from '../services/minglar.service';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
|
||||
import { HOST_SUGGESTION_TITLES } from '../../../common/utils/constants/minglar.constant';
|
||||
|
||||
const prismaService = new PrismaService();
|
||||
const minglarService = new MinglarService(prismaService);
|
||||
|
||||
interface AddSuggestionBody {
|
||||
hostXid: number;
|
||||
title: string;
|
||||
comments: string;
|
||||
activity_pqq_header_xid:number
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestion handler for host applications
|
||||
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
|
||||
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
|
||||
*/
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
// Verify authentication token
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) {
|
||||
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyMinglarAdminToken(token);
|
||||
|
||||
// Get user details
|
||||
const user = await prismaService.user.findUnique({
|
||||
where: { id: userInfo.id },
|
||||
select: { id: true, roleXid: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
let body: AddSuggestionBody;
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { title, comments , activity_pqq_header_xid} = body;
|
||||
|
||||
if (!title) {
|
||||
throw new ApiError(400, 'Title is required');
|
||||
}
|
||||
|
||||
if (!comments) {
|
||||
throw new ApiError(400, 'Comments are required');
|
||||
}
|
||||
|
||||
if(!activity_pqq_header_xid){
|
||||
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
|
||||
}
|
||||
|
||||
// Validate title is one of the allowed types
|
||||
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
|
||||
if (!allowedTitles.includes(title)) {
|
||||
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
|
||||
}
|
||||
|
||||
// Add suggestion using service
|
||||
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Suggestion added successfully',
|
||||
data: null,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
|
||||
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
|
||||
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../../common/utils/helper/ApiError';
|
||||
import { sendActivityAcceptanceMailtoHost } from '../../../../minglaradmin/services/approvalMailtoHost.service';
|
||||
import { MinglarService } from '../../../services/minglar.service';
|
||||
|
||||
const minglarService = new MinglarService(prismaClient);
|
||||
@@ -40,9 +39,6 @@ export const handler = safeHandler(async (
|
||||
Number(activityId),
|
||||
Number(userInfo.id)
|
||||
);
|
||||
const hostXid = await minglarService.getHostXidByActivityId(activityId)
|
||||
const hostDetails = await minglarService.getUserDetails(hostXid)
|
||||
await sendActivityAcceptanceMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
|
||||
|
||||
return {
|
||||
statusCode: 201,
|
||||
|
||||
@@ -10,12 +10,22 @@ export async function sendAMEmailForHostAssign(emailAddress: string, accountMana
|
||||
|
||||
const displayName = accountManagerName?.trim() || "there";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${displayName},</p>
|
||||
<p>A new host has been assigned to you by the Minglar team.</p>
|
||||
<p>You can now manage and support this host through your admin dashboard. Log in to review the host's details, connect with them, and take the next steps as needed.</p>
|
||||
<p>Warm regards,<br/>Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${displayName},</p>
|
||||
|
||||
<p>
|
||||
A new host has been assigned to you by the Minglar team.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can now manage and support this host through your admin dashboard. Log in to review the host’s details, connect with them, and take the next steps as needed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
|
||||
@@ -1,56 +1,75 @@
|
||||
import { brevoService } from "../../../common/email/brevoApi";
|
||||
import ApiError from "../../../common/utils/helper/ApiError";
|
||||
import config from "../../../config/config";
|
||||
import { brevoService } from '../../../common/email/brevoApi';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
export async function sendEmailToHostForApprovedApplication(
|
||||
emailAddress: string,
|
||||
name: string
|
||||
emailAddress: string,
|
||||
name: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
const subject = 'Host Onboarding Application Approved';
|
||||
|
||||
const subject = "Host Onboarding Application Approved";
|
||||
const htmlContent = `
|
||||
<p>Hi ${emailAddress},</p>
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
<p>We're pleased to inform you that your host onboarding application has been approved by our team.</p>
|
||||
<p>You can now proceed with completing your activity pre-qualification process.</p>
|
||||
<p>Please click the link below to log in to your account and continue:</p>
|
||||
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p>
|
||||
<p>If you have any questions or need assistance, feel free to reach out to us at info@minglargroup.com.</p>
|
||||
<p>Warm regards,<br/>Minglar Team</p>
|
||||
`;
|
||||
<p>
|
||||
We’re pleased to inform you that your host onboarding application has been approved by our team.
|
||||
</p>
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
<p>
|
||||
You can now proceed with completing your activity pre-qualification process.
|
||||
</p>
|
||||
|
||||
console.log("📧 Email sent successfully:", result);
|
||||
<p>
|
||||
Please click the link below to log in to your account and continue:
|
||||
</p>
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
|
||||
}
|
||||
<p>
|
||||
<strong>Host Portal Login</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions or need assistance, feel free to reach out to us at
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailToHostForMinglarApproval(
|
||||
emailAddress: string,
|
||||
emailAddress: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
const subject = 'Host Onboarding Application Approved';
|
||||
|
||||
const subject = "Host Onboarding Application Approved";
|
||||
|
||||
const htmlContent = `
|
||||
const htmlContent = `
|
||||
<p>Hi there,</p>
|
||||
<p>We're pleased to inform you that your host onboarding application has been approved by our team.</p>
|
||||
<p>You can now proceed with completing your activity pre-qualification process.</p>
|
||||
@@ -60,104 +79,199 @@ export async function sendEmailToHostForMinglarApproval(
|
||||
<p>Warm regards,<br/>Minglar Team</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log("📧 Email sent successfully:", result);
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
|
||||
}
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendAMPQQAcceptanceMailtoHost(
|
||||
emailAddress: string,
|
||||
name: string
|
||||
emailAddress: string,
|
||||
name: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
const subject = 'Your Activity Has Been Qualified for Onboarding';
|
||||
|
||||
const subject = "Your Activity Has Been Qualified for Onboarding";
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
<p>We're 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:</p>
|
||||
<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/>Minglar Team</p>
|
||||
`;
|
||||
<p>
|
||||
We’re pleased to inform you that your activity has been qualified on the Minglar platform.
|
||||
</p>
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
<p>
|
||||
You can now proceed to complete the details of your activity through the Host portal.
|
||||
</p>
|
||||
|
||||
console.log("📧 Email sent successfully:", result);
|
||||
<p>
|
||||
Please click the link below to log in to your account and continue:
|
||||
</p>
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
|
||||
}
|
||||
<p>
|
||||
<strong>Host Portal Login</strong><br/>
|
||||
<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
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendActivityAcceptanceMailtoHost(
|
||||
emailAddress: string,
|
||||
name: string
|
||||
emailAddress: string,
|
||||
name: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
sent: boolean;
|
||||
// messageId: string
|
||||
}> {
|
||||
const subject =
|
||||
'Onboarding Completed | You Can Now Set Up Your Activity Schedule and Listing';
|
||||
|
||||
const subject = "Onboarding Completed | You Can Now Set Up Your Activity Schedule and Listing";
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
<p>Great news!</p>
|
||||
<p>You have successfully completed the onboarding process for your activity on Minglar.</p>
|
||||
<p>You can now move on to the next step by setting up your activity's schedule. Once this is done, your activity will be ready to be listed on the Minglar app.</p>
|
||||
<p><strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
<p>If you have any questions or need assistance while setting things up, our team is here to help at info@minglargroup.com.</p>
|
||||
<p>We're excited to see your activity take shape and look forward to having it live on Minglar soon.</p>
|
||||
<p>Warm regards,<br/>Team Minglar</p>
|
||||
`;
|
||||
<p>Great news! 🎉</p>
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
<p>
|
||||
You have successfully completed the onboarding process for your activity on Minglar.
|
||||
</p>
|
||||
|
||||
console.log("📧 Email sent successfully:", result);
|
||||
<p>
|
||||
You can now move on to the next step by setting up your activity’s schedule. Once this is done, your activity will be ready to be listed on the Minglar app.
|
||||
</p>
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Brevo email send failed:", err);
|
||||
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
|
||||
}
|
||||
<p>
|
||||
👉 <strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions or need assistance while setting things up, our team is here to help at
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We’re excited to see your activity take shape and look forward to having it live on Minglar soon.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Team Minglar
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
// messageId: result.messageId
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendActivityScheduleApprovedMailtoHost(
|
||||
emailAddress: string,
|
||||
name: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
}> {
|
||||
const subject = 'Activity Schedule Approved | Let’s Go Live!!';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
<p>Your activity schedule has been officially approved.</p>
|
||||
|
||||
<p>
|
||||
Everything is now in place. Your experience is fully configured and queued for launch.
|
||||
Our team is completing the final activation before it goes live on Minglar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can continue managing your availability or adding new time slots anytime through your Host Portal:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
👉 <strong>Host Portal</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This is a big step. We’re excited to bring your experience to life on Minglar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
The Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send schedule approval email to host.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,15 +11,34 @@ export async function sendInvitationEmailForMinglarAdmin(
|
||||
|
||||
const subject = "Team Invitation: Account Manager at Minglar";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${emailAddress},</p>
|
||||
<p>We're happy to invite you to join the Minglar team as an Account Manager.</p>
|
||||
<p>To get started, please set up your account using the link below:</p>
|
||||
<p><a href="${link}" target="_blank">${link}</a></p>
|
||||
<p>If you have any questions or need help during the setup process, feel free to reach out.</p>
|
||||
<p>We look forward to working with you.</p>
|
||||
<p>Warm regards,<br/>Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${emailAddress},</p>
|
||||
|
||||
<p>
|
||||
We’re happy to invite you to join the Minglar team as an Account Manager.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To get started, please set up your account using the link below:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="${link}" target="_blank">${link}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions or need help during the setup process, feel free to reach out.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We look forward to working with you.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
|
||||
@@ -12,15 +12,27 @@ export async function sendEmailToHostForRejectedApplication(
|
||||
|
||||
const subject = "Action Needed: Host Onboarding Application";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${firstName},</p>
|
||||
<p>After reviewing your submission, we're unable to proceed at this stage, as some details require further updates. We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes.</p>
|
||||
<p><strong>Host portal login:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
<p>We appreciate your interest in Minglar and look forward to reviewing your updated application.</p>
|
||||
<p>Warm regards,<br/>Team Minglar</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${firstName},</p>
|
||||
|
||||
<p>
|
||||
After reviewing your submission, we’re unable to proceed at this stage, as some details require further updates. We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Host Portal Login</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We appreciate your interest in Minglar and look forward to reviewing your updated application.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
@@ -90,16 +102,40 @@ export async function sendAMPQQRejectionMailtoHost(
|
||||
|
||||
const subject = "Action Needed: Activity Pre-qualification";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
<p>Thank you for taking the time to submit your activity pre-qualification details on the Minglar platform.</p>
|
||||
<p>After reviewing your submission, we're 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>
|
||||
<p>You can log in to the Host portal to review the feedback and continue updating your application:</p>
|
||||
<p><a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a></p>
|
||||
<p>If you need any guidance, feel free to reach out to us at info@minglargroup.com.</p>
|
||||
<p>We appreciate your interest in partnering with Minglar and look forward to reviewing your updated submission.</p>
|
||||
<p>Thank you,<br/>Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
<p>
|
||||
Thank you for taking the time to submit your activity pre-qualification details on the Minglar platform.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
After reviewing your submission, we’re 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>
|
||||
|
||||
<p>
|
||||
You can log in to the Host portal to review the feedback and continue updating your application:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Host Portal Login</strong><br/>
|
||||
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you need any guidance, feel free to reach out to us at
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We appreciate your interest in partnering with Minglar and look forward to reviewing your updated submission.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thank you,<br/>
|
||||
Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
@@ -131,18 +167,40 @@ export async function sendActivityRejectionMailtoHost(
|
||||
|
||||
const subject = "Action Needed: Activity Onboarding";
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
<p>Thank you for submitting your activity for review.</p>
|
||||
<p>After evaluating the details provided, we're unable to approve the listing at this stage. A few updates are required before we can proceed.</p>
|
||||
<p>Please log in to your Host Portal to review the feedback and make the necessary revisions.</p>
|
||||
<p><strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
<p>Once the updates have been submitted, our team will re-evaluate your activity promptly.</p>
|
||||
<p>If you have any questions or need clarification on the feedback, feel free to reach out to us at info@minglargroup.com. We're happy to assist.</p>
|
||||
<p>Warm regards,<br/>The Minglar Team</p>
|
||||
`;
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
<p>
|
||||
Thank you for submitting your activity for review.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
After evaluating the details provided, we’re unable to approve the listing at this stage. A few updates are required before we can proceed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please log in to your Host Portal to review the feedback and make the necessary revisions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
👉 <strong>Access your Host Portal:</strong><br/>
|
||||
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once the updates have been submitted, our team will re-evaluate your activity promptly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions or need clarification on the feedback, feel free to reach out to us at
|
||||
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>. We’re happy to assist.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Warm regards,<br/>
|
||||
The Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
@@ -163,3 +221,57 @@ export async function sendActivityRejectionMailtoHost(
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendActivityScheduleRejectedMailtoHost(
|
||||
emailAddress: string,
|
||||
name: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
}> {
|
||||
const subject = 'Changes Required to Approve Your Activity Schedule';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi ${name},</p>
|
||||
|
||||
<p>Thank you for submitting your activity schedule for review.</p>
|
||||
|
||||
<p>
|
||||
At this stage, we’re unable to approve the schedule. Please log in to your Host Portal
|
||||
to review the changes required and update the details accordingly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
👉 <a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once the revisions have been made, our team will promptly review the schedule again so
|
||||
we can move you closer to going live on Minglar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We’re looking forward to activating your experience soon.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The Minglar Team
|
||||
</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
console.log('📧 Email sent successfully:', result);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send schedule rejection email to host.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
let body: {
|
||||
itineraryHeaderXid?: number;
|
||||
};
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { itineraryHeaderXid } = body;
|
||||
|
||||
if (!itineraryHeaderXid) {
|
||||
throw new ApiError(400, 'itineraryHeaderXid is required');
|
||||
}
|
||||
|
||||
const result = await itineraryService.getActivityDetailsAfterBooking(
|
||||
userId,
|
||||
itineraryHeaderXid,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Activity details retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
71
src/modules/user/handlers/itinerary/cancelUserItinerary.ts
Normal file
71
src/modules/user/handlers/itinerary/cancelUserItinerary.ts
Normal 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 { 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 itineraryHeaderXid =
|
||||
body.itineraryHeaderXid !== undefined && body.itineraryHeaderXid !== null
|
||||
? Number(body.itineraryHeaderXid)
|
||||
: NaN;
|
||||
const reason =
|
||||
typeof body.reason === 'string' ? body.reason.trim() : '';
|
||||
|
||||
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
|
||||
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
throw new ApiError(400, 'Cancellation reason is required.');
|
||||
}
|
||||
|
||||
const result = await itineraryService.cancelUserItinerary(
|
||||
userId,
|
||||
itineraryHeaderXid,
|
||||
reason,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Itinerary cancelled successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -7,6 +7,37 @@ import { ItineraryService } from '../../services/itinerary.service';
|
||||
|
||||
const itineraryService = new ItineraryService(prismaClient);
|
||||
|
||||
const parseQueryDate = (value: string, fieldName: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
const isoMatch = trimmedValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
|
||||
if (isoMatch) {
|
||||
const [, year, month, day] = isoMatch;
|
||||
const parsedDate = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
|
||||
if (!Number.isNaN(parsedDate.getTime())) {
|
||||
return parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const slashMatch = trimmedValue.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
|
||||
if (slashMatch) {
|
||||
const [, year, month, day] = slashMatch;
|
||||
const parsedDate = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
|
||||
if (!Number.isNaN(parsedDate.getTime())) {
|
||||
return parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const parsedDate = new Date(trimmedValue);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
throw new ApiError(400, `Invalid ${fieldName}`);
|
||||
}
|
||||
|
||||
return parsedDate;
|
||||
};
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
@@ -28,6 +59,7 @@ export const handler = safeHandler(async (
|
||||
|
||||
const itineraryHeaderXidRaw =
|
||||
event.queryStringParameters?.itineraryHeaderXid ?? null;
|
||||
const startDateRaw = event.queryStringParameters?.startDate ?? null;
|
||||
|
||||
let itineraryHeaderXid: number | undefined;
|
||||
if (
|
||||
@@ -42,9 +74,21 @@ export const handler = safeHandler(async (
|
||||
}
|
||||
}
|
||||
|
||||
const hasStartDate =
|
||||
startDateRaw !== null &&
|
||||
startDateRaw !== undefined &&
|
||||
startDateRaw.trim() !== '';
|
||||
|
||||
let startDate: Date | undefined;
|
||||
|
||||
if (hasStartDate) {
|
||||
startDate = parseQueryDate(startDateRaw, 'startDate');
|
||||
}
|
||||
|
||||
const result = await itineraryService.getAllUserSavedItineraries(
|
||||
userId,
|
||||
itineraryHeaderXid,
|
||||
startDate,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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');
|
||||
}
|
||||
|
||||
const itineraryHeaderXidRaw = event.queryStringParameters?.itineraryHeaderXid;
|
||||
if (!itineraryHeaderXidRaw) {
|
||||
throw new ApiError(400, 'itineraryHeaderXid is required.');
|
||||
}
|
||||
|
||||
const itineraryHeaderXid = Number(itineraryHeaderXidRaw);
|
||||
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
|
||||
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
|
||||
}
|
||||
|
||||
const result = await itineraryService.getItineraryCheckoutDetails(
|
||||
userId,
|
||||
itineraryHeaderXid,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Itinerary checkout details retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -75,6 +75,14 @@ export const handler = safeHandler(async (
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(payload.page) || payload.page <= 0) {
|
||||
throw new ApiError(400, 'page must be a positive integer.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(payload.limit) || payload.limit <= 0) {
|
||||
throw new ApiError(400, 'limit must be a positive integer.');
|
||||
}
|
||||
|
||||
const result = await itineraryService.getMatchingBucketInterestedActivities(
|
||||
userId,
|
||||
payload,
|
||||
|
||||
@@ -35,24 +35,14 @@ export const handler = safeHandler(async (
|
||||
}
|
||||
}
|
||||
|
||||
const itineraryActivityXid = Number(body.itineraryActivityXid);
|
||||
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
|
||||
throw new ApiError(400, 'itineraryActivityXid is required.');
|
||||
}
|
||||
const activities = Array.isArray(body.activities)
|
||||
? body.activities
|
||||
: body.itineraryActivityXid !== undefined
|
||||
? [body]
|
||||
: [];
|
||||
|
||||
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.');
|
||||
if (!activities.length) {
|
||||
throw new ApiError(400, 'activities is required and must be a non-empty array.');
|
||||
}
|
||||
|
||||
const toOptionalId = (value: unknown) => {
|
||||
@@ -68,21 +58,60 @@ export const handler = safeHandler(async (
|
||||
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,
|
||||
const normalizedActivities = activities.map((activity: any, index: number) => {
|
||||
const itineraryActivityXid = Number(activity.itineraryActivityXid);
|
||||
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
|
||||
throw new ApiError(400, `activities[${index}].itineraryActivityXid is required.`);
|
||||
}
|
||||
|
||||
const selectedFoodTypeIds: number[] = Array.isArray(activity.selectedFoodTypeIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
activity.selectedFoodTypeIds.map((id: unknown): number => Number(id)),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
const selectedEquipmentIds: number[] = Array.isArray(activity.selectedEquipmentIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
activity.selectedEquipmentIds.map((id: unknown): number => Number(id)),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itineraryActivityXid,
|
||||
isFoodOpted: activity.isFoodOpted === undefined ? false : Boolean(activity.isFoodOpted),
|
||||
selectedFoodTypeIds,
|
||||
isTrainerOpted: activity.isTrainerOpted === undefined ? false : Boolean(activity.isTrainerOpted),
|
||||
isInActivityNavigationOpted:
|
||||
activity.isInActivityNavigationOpted === undefined
|
||||
? false
|
||||
: Boolean(activity.isInActivityNavigationOpted),
|
||||
selectedNavigationModeXid: toOptionalId(activity.selectedNavigationModeXid),
|
||||
selectedEquipmentIds,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await itineraryService.saveItineraryActivitySelections(
|
||||
userId,
|
||||
normalizedActivities,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
|
||||
63
src/modules/user/handlers/payment/razorpayWebhook.ts
Normal file
63
src/modules/user/handlers/payment/razorpayWebhook.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { PaymentService } from '../../services/payment.service';
|
||||
|
||||
const paymentService = new PaymentService(prismaClient);
|
||||
|
||||
function getHeaderValue(
|
||||
headers: APIGatewayProxyEvent['headers'],
|
||||
name: string,
|
||||
): string | undefined {
|
||||
const targetName = name.toLowerCase();
|
||||
|
||||
for (const [key, value] of Object.entries(headers ?? {})) {
|
||||
if (key.toLowerCase() === targetName) {
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const signature =
|
||||
getHeaderValue(event.headers, 'x-razorpay-signature')?.trim() ?? '';
|
||||
|
||||
if (!signature) {
|
||||
throw new ApiError(400, 'Missing Razorpay webhook signature.');
|
||||
}
|
||||
|
||||
const rawBody = event.body
|
||||
? event.isBase64Encoded
|
||||
? Buffer.from(event.body, 'base64').toString('utf8')
|
||||
: event.body
|
||||
: '';
|
||||
|
||||
const result = await paymentService.handleWebhook({
|
||||
rawBody,
|
||||
signature,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Webhook received',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -64,7 +64,7 @@ export const handler = safeHandler(async (
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Payment verified',
|
||||
message: 'Payment verified and itinerary booked successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,61 @@ import crypto from 'crypto';
|
||||
import Razorpay from 'razorpay';
|
||||
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ItineraryService } from './itinerary.service';
|
||||
|
||||
const razorpay = new Razorpay({
|
||||
key_id: process.env.RAZORPAY_KEY_ID!,
|
||||
key_secret: process.env.RAZORPAY_KEY_SECRET!,
|
||||
key_id: config.RAZORPAY_KEY_ID,
|
||||
key_secret: config.RAZORPAY_KEY_SECRET,
|
||||
});
|
||||
|
||||
const buildUniqueReceipt = (input?: string) => {
|
||||
const normalizedPrefix = (input?.trim() || 'receipt')
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.slice(0, 20);
|
||||
const timePart = Date.now().toString(36);
|
||||
const randomPart = crypto.randomBytes(4).toString('hex');
|
||||
|
||||
return `${normalizedPrefix}_${timePart}_${randomPart}`.slice(0, 100);
|
||||
};
|
||||
|
||||
type RazorpayWebhookPayload = {
|
||||
event?: string;
|
||||
payload?: {
|
||||
payment?: {
|
||||
entity?: {
|
||||
id?: string;
|
||||
order_id?: string;
|
||||
status?: string;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
};
|
||||
};
|
||||
order?: {
|
||||
entity?: {
|
||||
id?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
private signaturesMatch(expectedSignature: string, receivedSignature: string) {
|
||||
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
|
||||
const receivedBuffer = Buffer.from(receivedSignature, 'utf8');
|
||||
|
||||
if (expectedBuffer.length !== receivedBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
|
||||
}
|
||||
|
||||
async createOrder(
|
||||
userXid: number,
|
||||
payload: {
|
||||
@@ -57,7 +102,7 @@ export class PaymentService {
|
||||
}
|
||||
|
||||
const amountInPaise = Math.round(payload.amount * 100);
|
||||
const receipt = payload.receipt ?? `receipt_${Date.now()}`;
|
||||
const receipt = buildUniqueReceipt(payload.receipt);
|
||||
const currency = payload.currency ?? 'INR';
|
||||
|
||||
const order = (await razorpay.orders.create({
|
||||
@@ -125,45 +170,65 @@ export class PaymentService {
|
||||
throw new ApiError(400, 'Invalid signature.');
|
||||
}
|
||||
|
||||
const paymentOrder = await this.prisma.paymentOrders.findFirst({
|
||||
where: {
|
||||
razorpayOrderId: orderId,
|
||||
userXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
const itineraryService = new ItineraryService(this.prisma);
|
||||
|
||||
if (!paymentOrder) {
|
||||
throw new ApiError(404, 'Payment order not found.');
|
||||
}
|
||||
const { updatedPaymentOrder, bookingResult } = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const paymentOrder = await tx.paymentOrders.findFirst({
|
||||
where: {
|
||||
razorpayOrderId: orderId,
|
||||
userXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
if (!paymentOrder) {
|
||||
throw new ApiError(404, 'Payment order not found.');
|
||||
}
|
||||
|
||||
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
let updatedPayment = paymentOrder;
|
||||
|
||||
if (paymentOrder.paymentStatus === 'paid') {
|
||||
if (
|
||||
paymentOrder.razorpayPaymentId &&
|
||||
paymentOrder.razorpayPaymentId !== paymentId
|
||||
) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Payment order is already verified with a different payment id.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
updatedPayment = await tx.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
},
|
||||
data: {
|
||||
razorpayPaymentId: paymentId,
|
||||
razorpaySignature: signature,
|
||||
paymentStatus: 'paid',
|
||||
verifiedAt: new Date(),
|
||||
paidAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let booking = null;
|
||||
if (updatedPayment.itineraryHeaderXid) {
|
||||
booking = await itineraryService.bookItineraryAfterPayment(
|
||||
tx,
|
||||
userXid,
|
||||
updatedPayment.itineraryHeaderXid,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
updatedPaymentOrder: updatedPayment,
|
||||
bookingResult: booking,
|
||||
};
|
||||
},
|
||||
data: {
|
||||
razorpayPaymentId: paymentId,
|
||||
razorpaySignature: signature,
|
||||
paymentStatus: 'paid',
|
||||
verifiedAt: new Date(),
|
||||
paidAt: new Date(),
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
return {
|
||||
paymentOrderId: updatedPaymentOrder.id,
|
||||
@@ -172,6 +237,159 @@ export class PaymentService {
|
||||
status: updatedPaymentOrder.paymentStatus,
|
||||
verifiedAt: updatedPaymentOrder.verifiedAt,
|
||||
paidAt: updatedPaymentOrder.paidAt,
|
||||
itineraryBooking: bookingResult,
|
||||
};
|
||||
}
|
||||
|
||||
async handleWebhook(payload: {
|
||||
rawBody: string;
|
||||
signature: string;
|
||||
}) {
|
||||
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET?.trim();
|
||||
|
||||
if (!webhookSecret) {
|
||||
throw new ApiError(
|
||||
500,
|
||||
'Razorpay webhook secret is not configured.',
|
||||
);
|
||||
}
|
||||
|
||||
const rawBody = payload.rawBody;
|
||||
const signature = payload.signature?.trim();
|
||||
|
||||
if (!rawBody) {
|
||||
throw new ApiError(400, 'Webhook body is required.');
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
throw new ApiError(400, 'Razorpay webhook signature is required.');
|
||||
}
|
||||
|
||||
const generatedSignature = crypto
|
||||
.createHmac('sha256', webhookSecret)
|
||||
.update(rawBody, 'utf8')
|
||||
.digest('hex');
|
||||
|
||||
if (!this.signaturesMatch(generatedSignature, signature)) {
|
||||
throw new ApiError(400, 'Invalid webhook signature.');
|
||||
}
|
||||
|
||||
let body: RazorpayWebhookPayload;
|
||||
try {
|
||||
body = JSON.parse(rawBody) as RazorpayWebhookPayload;
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid webhook JSON body.');
|
||||
}
|
||||
|
||||
const eventType = body.event?.trim();
|
||||
if (!eventType) {
|
||||
throw new ApiError(400, 'Webhook event type is missing.');
|
||||
}
|
||||
|
||||
const paymentEntity = body.payload?.payment?.entity;
|
||||
const orderEntity = body.payload?.order?.entity;
|
||||
const razorpayOrderId =
|
||||
paymentEntity?.order_id?.trim() || orderEntity?.id?.trim();
|
||||
const razorpayPaymentId = paymentEntity?.id?.trim();
|
||||
|
||||
if (!razorpayOrderId) {
|
||||
throw new ApiError(400, 'Webhook payload is missing Razorpay order id.');
|
||||
}
|
||||
|
||||
const paymentOrder = await this.prisma.paymentOrders.findFirst({
|
||||
where: {
|
||||
razorpayOrderId,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!paymentOrder) {
|
||||
return {
|
||||
eventType,
|
||||
processed: false,
|
||||
message: 'Payment order not found for webhook payload.',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
eventType === 'payment.captured' &&
|
||||
paymentOrder.paymentStatus === 'paid' &&
|
||||
paymentOrder.razorpayPaymentId === razorpayPaymentId
|
||||
) {
|
||||
return {
|
||||
paymentOrderId: paymentOrder.id,
|
||||
orderId: paymentOrder.razorpayOrderId,
|
||||
paymentId: paymentOrder.razorpayPaymentId,
|
||||
status: paymentOrder.paymentStatus,
|
||||
verifiedAt: paymentOrder.verifiedAt,
|
||||
paidAt: paymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType === 'payment.captured' || eventType === 'order.paid') {
|
||||
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
},
|
||||
data: {
|
||||
...(razorpayPaymentId
|
||||
? { razorpayPaymentId }
|
||||
: {}),
|
||||
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,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType === 'payment.failed') {
|
||||
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
},
|
||||
data: {
|
||||
...(razorpayPaymentId
|
||||
? { razorpayPaymentId }
|
||||
: {}),
|
||||
paymentStatus: 'failed',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
paymentOrderId: updatedPaymentOrder.id,
|
||||
orderId: updatedPaymentOrder.razorpayOrderId,
|
||||
paymentId: updatedPaymentOrder.razorpayPaymentId,
|
||||
status: updatedPaymentOrder.paymentStatus,
|
||||
verifiedAt: updatedPaymentOrder.verifiedAt,
|
||||
paidAt: updatedPaymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
paymentOrderId: paymentOrder.id,
|
||||
orderId: paymentOrder.razorpayOrderId,
|
||||
paymentId: paymentOrder.razorpayPaymentId,
|
||||
status: paymentOrder.paymentStatus,
|
||||
verifiedAt: paymentOrder.verifiedAt,
|
||||
paidAt: paymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
55
src/modules/websocket/handlers/connect.ts
Normal file
55
src/modules/websocket/handlers/connect.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../common/database/prisma.lambda.service';
|
||||
import { verifyAnyToken } from '../../../common/middlewares/jwt/authForAny';
|
||||
import { WebSocketService } from '../../../common/services/websocket.service';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
const wsService = new WebSocketService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(
|
||||
async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const connectionId = event.requestContext.connectionId;
|
||||
if (!connectionId) {
|
||||
throw new ApiError(400, 'Missing connection ID');
|
||||
}
|
||||
|
||||
const token =
|
||||
event.queryStringParameters?.token ||
|
||||
event.queryStringParameters?.['x-auth-token'] ||
|
||||
event.headers?.['x-auth-token'] ||
|
||||
event.headers?.['X-Auth-Token'];
|
||||
|
||||
if (!token) {
|
||||
throw new ApiError(401, 'Token is required for WebSocket connection');
|
||||
}
|
||||
|
||||
const userInfo = await verifyAnyToken(token);
|
||||
|
||||
const activityXidParam =
|
||||
event.queryStringParameters?.activityXid ||
|
||||
event.queryStringParameters?.activity_xid;
|
||||
const activityXid = activityXidParam ? Number(activityXidParam) : null;
|
||||
|
||||
if (activityXidParam && (!activityXid || isNaN(activityXid))) {
|
||||
throw new ApiError(400, 'Invalid activityXid');
|
||||
}
|
||||
|
||||
await wsService.connect({
|
||||
connectionId,
|
||||
userXid: userInfo.id,
|
||||
activityXid,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connected',
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
17
src/modules/websocket/handlers/default.ts
Normal file
17
src/modules/websocket/handlers/default.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
|
||||
export const handler = safeHandler(
|
||||
async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Default route',
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
26
src/modules/websocket/handlers/disconnect.ts
Normal file
26
src/modules/websocket/handlers/disconnect.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../common/database/prisma.lambda.service';
|
||||
import { WebSocketService } from '../../../common/services/websocket.service';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
|
||||
const wsService = new WebSocketService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(
|
||||
async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const connectionId = event.requestContext.connectionId;
|
||||
if (connectionId) {
|
||||
await wsService.disconnect(connectionId);
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Disconnected',
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
99
src/modules/websocket/handlers/getMessages.ts
Normal file
99
src/modules/websocket/handlers/getMessages.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import AWS from 'aws-sdk';
|
||||
import { prismaClient } from '../../../common/database/prisma.lambda.service';
|
||||
import { ChatService } from '../../../common/services/chat.service';
|
||||
import { WebSocketService } from '../../../common/services/websocket.service';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
const chatService = new ChatService(prismaClient);
|
||||
const wsService = new WebSocketService(prismaClient);
|
||||
|
||||
function getApiGatewayEndpoint(event: APIGatewayProxyEvent): string {
|
||||
const domainName = event.requestContext.domainName;
|
||||
const stage = event.requestContext.stage;
|
||||
const isLocal =
|
||||
domainName?.includes('localhost') || domainName?.includes('127.0.0.1');
|
||||
const protocol = isLocal ? 'http' : 'https';
|
||||
return `${protocol}://${domainName}/${stage}`;
|
||||
}
|
||||
|
||||
async function postToConnection(
|
||||
api: AWS.ApiGatewayManagementApi,
|
||||
connectionId: string,
|
||||
payload: unknown
|
||||
) {
|
||||
await api
|
||||
.postToConnection({
|
||||
ConnectionId: connectionId,
|
||||
Data: Buffer.from(JSON.stringify(payload)),
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
|
||||
export const handler = safeHandler(
|
||||
async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const connectionId = event.requestContext.connectionId;
|
||||
if (!connectionId) {
|
||||
throw new ApiError(400, 'Missing connection ID');
|
||||
}
|
||||
|
||||
let body: any;
|
||||
try {
|
||||
body = JSON.parse(event.body || '{}');
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const activityXid = Number(body.activityXid ?? body.activity_xid);
|
||||
const otherUserXid = Number(body.otherUserXid ?? body.other_user_xid);
|
||||
const limit = body.limit ? Number(body.limit) : undefined;
|
||||
|
||||
if (!activityXid || isNaN(activityXid)) {
|
||||
throw new ApiError(400, 'Valid activityXid is required');
|
||||
}
|
||||
|
||||
if (!otherUserXid || isNaN(otherUserXid)) {
|
||||
throw new ApiError(400, 'Valid otherUserXid is required');
|
||||
}
|
||||
|
||||
const connection = await wsService.getConnectionById(connectionId);
|
||||
if (!connection) {
|
||||
throw new ApiError(401, 'Unauthorized WebSocket connection');
|
||||
}
|
||||
|
||||
const userXid = connection.userXid;
|
||||
|
||||
const messages = await chatService.getMessages({
|
||||
activityXid,
|
||||
userXid,
|
||||
otherUserXid,
|
||||
limit,
|
||||
});
|
||||
|
||||
const endpoint = getApiGatewayEndpoint(event);
|
||||
const api = new AWS.ApiGatewayManagementApi({ endpoint });
|
||||
const payload = { type: 'messages', data: messages };
|
||||
|
||||
try {
|
||||
await postToConnection(api, connectionId, payload);
|
||||
} catch (err: any) {
|
||||
if (err?.statusCode === 410) {
|
||||
await wsService.disconnect(connectionId);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Messages sent',
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
111
src/modules/websocket/handlers/sendMessage.ts
Normal file
111
src/modules/websocket/handlers/sendMessage.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import AWS from 'aws-sdk';
|
||||
import { prismaClient } from '../../../common/database/prisma.lambda.service';
|
||||
import { ChatService } from '../../../common/services/chat.service';
|
||||
import { WebSocketService } from '../../../common/services/websocket.service';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
const chatService = new ChatService(prismaClient);
|
||||
const wsService = new WebSocketService(prismaClient);
|
||||
|
||||
function getApiGatewayEndpoint(event: APIGatewayProxyEvent): string {
|
||||
const domainName = event.requestContext.domainName;
|
||||
const stage = event.requestContext.stage;
|
||||
const isLocal =
|
||||
domainName?.includes('localhost') || domainName?.includes('127.0.0.1');
|
||||
const protocol = isLocal ? 'http' : 'https';
|
||||
return `${protocol}://${domainName}/${stage}`;
|
||||
}
|
||||
|
||||
async function postToConnection(
|
||||
api: AWS.ApiGatewayManagementApi,
|
||||
connectionId: string,
|
||||
payload: unknown
|
||||
) {
|
||||
await api
|
||||
.postToConnection({
|
||||
ConnectionId: connectionId,
|
||||
Data: Buffer.from(JSON.stringify(payload)),
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
|
||||
export const handler = safeHandler(
|
||||
async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const connectionId = event.requestContext.connectionId;
|
||||
if (!connectionId) {
|
||||
throw new ApiError(400, 'Missing connection ID');
|
||||
}
|
||||
|
||||
let body: any;
|
||||
try {
|
||||
body = JSON.parse(event.body || '{}');
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const activityXid = Number(body.activityXid ?? body.activity_xid);
|
||||
const receiverXid = Number(
|
||||
body.receiverXid ?? body.receivedXid ?? body.received_xid
|
||||
);
|
||||
const message = body.message;
|
||||
|
||||
if (!activityXid || isNaN(activityXid)) {
|
||||
throw new ApiError(400, 'Valid activityXid is required');
|
||||
}
|
||||
|
||||
if (!receiverXid || isNaN(receiverXid)) {
|
||||
throw new ApiError(400, 'Valid receiverXid is required');
|
||||
}
|
||||
|
||||
const connection = await wsService.getConnectionById(connectionId);
|
||||
if (!connection) {
|
||||
throw new ApiError(401, 'Unauthorized WebSocket connection');
|
||||
}
|
||||
|
||||
const senderXid = connection.userXid;
|
||||
|
||||
const saved = await chatService.sendMessage({
|
||||
activityXid,
|
||||
senderXid,
|
||||
receiverXid,
|
||||
message,
|
||||
});
|
||||
|
||||
const endpoint = getApiGatewayEndpoint(event);
|
||||
const api = new AWS.ApiGatewayManagementApi({ endpoint });
|
||||
|
||||
const [receiverConnections, senderConnections] = await Promise.all([
|
||||
wsService.getConnectionsForUser({ userXid: receiverXid, activityXid }),
|
||||
wsService.getConnectionsForUser({ userXid: senderXid, activityXid }),
|
||||
]);
|
||||
|
||||
const targets = [...receiverConnections, ...senderConnections];
|
||||
const payload = { type: 'message', data: saved };
|
||||
|
||||
for (const target of targets) {
|
||||
try {
|
||||
await postToConnection(api, target.connectionId, payload);
|
||||
} catch (err: any) {
|
||||
if (err?.statusCode === 410) {
|
||||
await wsService.disconnect(target.connectionId);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Message sent',
|
||||
data: saved,
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user