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

This commit is contained in:
paritosh18
2026-04-23 18:41:39 +05:30
16 changed files with 1618 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { 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,
}),
};
});

View File

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

View File

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