diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a8ea901..b26f263 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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[] @@ -677,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") @@ -809,6 +813,8 @@ model HostHeader { HostBankDetails HostBankDetails[] HostDocuments HostDocuments[] HostSuggestion HostSuggestion[] + hostMembers HostMembers[] + hostRolePermissionMasters HostRolePermissionMasters[] hostParent HostParent[] HostTrack HostTrack[] Activities Activities[] @@ -840,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") @@ -1056,6 +1139,7 @@ model Activities { activityPickUpTransports ActivityPickUpTransport[] userBucketInterests UserBucketInterested[] activityMessages ActivityMessages[] + assignedHostMembers HostMemberActivities[] @@map("activities") @@schema("act") @@ -1695,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") diff --git a/prisma/seed.ts b/prisma/seed.ts index 5976167..c081d04 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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' }, diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index 528d2e9..685bcd7 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -308,6 +308,86 @@ 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 + # Functions with S3/AWS SDK dependencies submitCompanyDetails: handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 6c15442..99cb242 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -498,6 +498,21 @@ 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 + createRazorpayOrder: handler: src/modules/user/handlers/payment/createOrder.handler memorySize: 512 diff --git a/src/modules/host/handlers/settings/getAllInvitedCoadminAndOperator.ts b/src/modules/host/handlers/settings/getAllInvitedCoadminAndOperator.ts new file mode 100644 index 0000000..1b2e332 --- /dev/null +++ b/src/modules/host/handlers/settings/getAllInvitedCoadminAndOperator.ts @@ -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 => { + 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, + }), + }; +}); diff --git a/src/modules/host/handlers/settings/getMemberRoles.ts b/src/modules/host/handlers/settings/getMemberRoles.ts new file mode 100644 index 0000000..0f61e58 --- /dev/null +++ b/src/modules/host/handlers/settings/getMemberRoles.ts @@ -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 => { + 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, + }, + }), + }; +}); diff --git a/src/modules/host/handlers/settings/getPermissionMasters.ts b/src/modules/host/handlers/settings/getPermissionMasters.ts new file mode 100644 index 0000000..8656fad --- /dev/null +++ b/src/modules/host/handlers/settings/getPermissionMasters.ts @@ -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 => { + 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, + }, + }), + }; +}); diff --git a/src/modules/host/handlers/settings/inviteMember.ts b/src/modules/host/handlers/settings/inviteMember.ts new file mode 100644 index 0000000..c9d7421 --- /dev/null +++ b/src/modules/host/handlers/settings/inviteMember.ts @@ -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 => { + 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 = {}; + 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, + }, + }), + }; +}); diff --git a/src/modules/host/handlers/settings/saveRolePermissions.ts b/src/modules/host/handlers/settings/saveRolePermissions.ts new file mode 100644 index 0000000..0455bec --- /dev/null +++ b/src/modules/host/handlers/settings/saveRolePermissions.ts @@ -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 => { + 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 = {}; + 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, + }, + }), + }; +}); diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index a511f36..4cad483 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -1142,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, @@ -1168,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 }, ); diff --git a/src/modules/host/services/hostMember.service.ts b/src/modules/host/services/hostMember.service.ts new file mode 100644 index 0000000..cafea46 --- /dev/null +++ b/src/modules/host/services/hostMember.service.ts @@ -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, + }; + }); + } +} diff --git a/src/modules/host/services/hostRolePermission.service.ts b/src/modules/host/services/hostRolePermission.service.ts new file mode 100644 index 0000000..668e938 --- /dev/null +++ b/src/modules/host/services/hostRolePermission.service.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +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, + }; + }); + } +} diff --git a/src/modules/host/services/sendHostMemberInvitationEmail.service.ts b/src/modules/host/services/sendHostMemberInvitationEmail.service.ts new file mode 100644 index 0000000..fcf2393 --- /dev/null +++ b/src/modules/host/services/sendHostMemberInvitationEmail.service.ts @@ -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 + ? `
    ${permissionLabels.map((permission) => `
  • ${permission}
  • `).join('')}
` + : '

No permissions were assigned.

'; + + const activitiesHtml = activityNames.length + ? `
    ${activityNames.map((activity) => `
  • ${activity}
  • `).join('')}
` + : '

No activities were assigned.

'; + + const htmlContent = ` +

Hi there,

+

You have been invited by ${hostName} to join the Minglar Host portal as ${memberRole}.

+

The following permissions have been assigned to your account:

+ ${permissionsHtml} +

The following activities have been assigned to you:

+ ${activitiesHtml} +

You can access the host portal using the link below:

+

${config.HOST_LINK}

+

If you were not expecting this invitation, you can ignore this email.

+

Warm regards,
Team Minglar

+ `; + + 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.'); + } +} diff --git a/src/modules/user/handlers/itinerary/cancelUserItinerary.ts b/src/modules/user/handlers/itinerary/cancelUserItinerary.ts new file mode 100644 index 0000000..7c9b3f2 --- /dev/null +++ b/src/modules/user/handlers/itinerary/cancelUserItinerary.ts @@ -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 => { + 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 = {}; + 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, + }), + }; +}); diff --git a/src/modules/user/services/itinerary.service.ts b/src/modules/user/services/itinerary.service.ts index 37b3821..3f5aeb2 100644 --- a/src/modules/user/services/itinerary.service.ts +++ b/src/modules/user/services/itinerary.service.ts @@ -2462,6 +2462,207 @@ export class ItineraryService { }; } + async cancelUserItinerary( + userXid: number, + itineraryHeaderXid: number, + cancellationReason: string, + ) { + return this.prisma.$transaction(async (tx) => { + const itinerary = await tx.itineraryHeader.findFirst({ + where: { + id: itineraryHeaderXid, + ownerXid: userXid, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + itineraryNo: true, + title: true, + itineraryStatus: true, + }, + }); + + if (!itinerary) { + throw new ApiError( + 404, + 'Active itinerary not found for the logged-in user.', + ); + } + + const itineraryActivityIds = ( + await tx.itineraryActivities.findMany({ + where: { + itineraryHeaderXid, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + }, + }) + ).map((item) => item.id); + + const itineraryDetailIds = itineraryActivityIds.length + ? ( + await tx.itineraryDetails.findMany({ + where: { + itineraryActivityXid: { + in: itineraryActivityIds, + }, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + }, + }) + ).map((item) => item.id) + : []; + + const itinerarySelectionIds = itineraryActivityIds.length + ? ( + await tx.itineraryActivitySelection.findMany({ + where: { + itineraryActivityXid: { + in: itineraryActivityIds, + }, + isActive: true, + deletedAt: null, + }, + select: { + id: true, + }, + }) + ).map((item) => item.id) + : []; + + if (itineraryDetailIds.length) { + await tx.itineraryDetailTaxes.updateMany({ + where: { + itineraryDetailXid: { + in: itineraryDetailIds, + }, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + }, + }); + + await tx.itineraryDetails.updateMany({ + where: { + id: { + in: itineraryDetailIds, + }, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + itineraryStatus: 'cancelled', + }, + }); + } + + if (itinerarySelectionIds.length) { + await tx.itineraryActivitySelectionFoodType.updateMany({ + where: { + itineraryActivitySelectionXid: { + in: itinerarySelectionIds, + }, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + }, + }); + + await tx.itineraryActivitySelectionEquipment.updateMany({ + where: { + itineraryActivitySelectionXid: { + in: itinerarySelectionIds, + }, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + }, + }); + + await tx.itineraryActivitySelection.updateMany({ + where: { + id: { + in: itinerarySelectionIds, + }, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + }, + }); + } + + await tx.itineraryActivities.updateMany({ + where: { + itineraryHeaderXid, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + bookingStatus: 'cancelled', + }, + }); + + await tx.itineraryStartStopDetails.updateMany({ + where: { + itineraryHeaderXid, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + }, + }); + + await tx.itineraryMembers.updateMany({ + where: { + itineraryHeaderXid, + isActive: true, + deletedAt: null, + }, + data: { + isActive: false, + memberStatus: 'cancelled', + }, + }); + + await tx.$executeRaw` + UPDATE "itn"."itinerary_header" + SET + "is_active" = false, + "itinerary_status" = 'cancelled', + "cancellation_reason" = ${cancellationReason}, + "updated_at" = NOW() + WHERE "id" = ${itineraryHeaderXid} + `; + + return { + itineraryHeaderXid: itinerary.id, + itineraryNo: itinerary.itineraryNo, + title: itinerary.title, + itineraryStatus: 'cancelled', + cancellationReason, + isActive: false, + }; + }); + } + async bookItineraryAfterPayment( tx: Prisma.TransactionClient, userXid: number, diff --git a/src/modules/user/services/payment.service.ts b/src/modules/user/services/payment.service.ts index 6f8c662..c0a9603 100644 --- a/src/modules/user/services/payment.service.ts +++ b/src/modules/user/services/payment.service.ts @@ -12,6 +12,17 @@ const razorpay = new Razorpay({ 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?: { @@ -91,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({