5 Commits

Author SHA1 Message Date
paritosh18
841539b8cc Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-04-23 18:41:39 +05:30
paritosh18
5fcff67916 add oprator apis 2026-04-23 18:41:37 +05:30
paritosh18
acd31725ed added reason in cancel api 2026-04-20 20:04:35 +05:30
paritosh18
0d96b1e67e payment serive 2026-04-20 18:57:46 +05:30
paritosh18
f98354a1c8 added cancel iteneray api 2026-04-20 17:25:49 +05:30
16 changed files with 1063 additions and 4 deletions

View File

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

12
serverless.operator.yml Normal file
View File

@@ -0,0 +1,12 @@
service: minglar-operator
useDotenv: ${file(./serverless/common.yml):useDotenv}
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider}
build: ${file(./serverless/common.yml):build}
package: ${file(./serverless/common.yml):package}
plugins: ${file(./serverless/common.yml):plugins}
custom: ${file(./serverless/common.yml):custom}
functions:
- ${file(./serverless/functions/operator.yml)}

View File

@@ -155,6 +155,7 @@ package:
# Import function definitions from separate files organized by module
functions:
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/operator.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/user.yml)}

View File

@@ -511,7 +511,6 @@ resendOTPmail:
path: /resend-otp
method: post
mediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadToS3.handler
memorySize: 512
@@ -527,7 +526,6 @@ mediaUploadTos3:
path: /media/upload/activity/{activityXid}
method: post
venueMediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
memorySize: 512
@@ -543,7 +541,6 @@ venueMediaUploadTos3:
path: /media/upload/venue/activity/{activityXid}
method: post
mediaDeleteFroms3:
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
memorySize: 512

View File

@@ -0,0 +1,73 @@
# Operator Module Functions
operatorSignUp:
handler: src/modules/host/handlers/operator/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/signup
method: post
operatorVerifyOtp:
handler: src/modules/host/handlers/operator/verifyOtp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/verify-otp
method: post
operatorCreatePassword:
handler: src/modules/host/handlers/operator/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/create-password
method: post
operatorLogin:
handler: src/modules/host/handlers/operator/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/login
method: post

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,112 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { NextFunction, Request, Response } from 'express';
import { prisma } from '../../database/prisma.client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '../../utils/constants/common.constant';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
interface UserPayload {
id: string;
role?: string;
}
declare module 'express-serve-static-core' {
interface Request {
user?: UserPayload;
}
}
export async function verifyOperatorToken(
token: string,
): Promise<{ id: number; role?: string }> {
if (!token) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
if (!userId) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
const user = await prisma.user.findUnique({
where: { id: userId },
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: { userXid: userId },
orderBy: { id: 'desc' },
});
if (latestToken?.isBlackListed === true) {
throw new ApiError(401, 'This session is expired. Please login.');
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
if (user.roleXid !== ROLE.OPERATOR) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
return { id: user.id, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(
httpStatus.UNAUTHORIZED,
'Your session has expired. Please log in again.',
);
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
}
}
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void,
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
try {
const userInfo = await verifyOperatorToken(token);
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
const authForOperator =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForOperator;

View File

@@ -0,0 +1,58 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForOperator';
import { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
const userInfo = await verifyOperatorToken(token);
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await operatorAuthService.createOperatorPassword(userInfo.id, password);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Password created successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,55 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { GetHostLoginResponseDTO } from '../../dto/host.dto';
import { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; userPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const operator = await operatorAuthService.loginForOperator(
emailAddress.trim().toLowerCase(),
userPassword,
);
const generatedToken = await tokenService.generateAuthToken(operator.id);
const response = new GetHostLoginResponseDTO(
operator,
generatedToken.access.token,
generatedToken.refresh.token,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Login successful',
data: response,
}),
};
});

View File

@@ -0,0 +1,53 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: {
firstName?: string;
lastName?: string;
emailAddress?: string;
isdCode?: string;
mobileNumber?: string;
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { firstName, lastName, emailAddress, isdCode, mobileNumber } = body;
if (!emailAddress && !mobileNumber) {
throw new ApiError(400, 'Email address or mobile number is required');
}
const signupResponse = await operatorAuthService.signUpOperator({
firstName,
lastName,
emailAddress,
isdCode,
mobileNumber,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully',
data: signupResponse,
}),
};
});

View File

@@ -0,0 +1,51 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; mobileNumber?: string; otp?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, mobileNumber, otp } = body;
if ((!emailAddress && !mobileNumber) || !otp) {
throw new ApiError(400, 'Email address or mobile number and OTP are required');
}
const operator = await operatorAuthService.verifyOperatorOtp({
emailAddress,
mobileNumber,
otp,
});
const generatedToken = await tokenService.generateAuthToken(operator.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP verified successfully',
accessToken: generatedToken.access.token,
refreshToken: generatedToken.refresh.token,
data: null,
}),
};
});

View File

@@ -0,0 +1,257 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import ApiError from '../../../common/utils/helper/ApiError';
import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator';
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
type OperatorSignupInput = {
firstName?: string;
lastName?: string;
emailAddress?: string;
isdCode?: string;
mobileNumber?: string;
};
@Injectable()
export class OperatorAuthService {
constructor(private prisma: PrismaClient) {}
async findInvitedOperator(input: {
emailAddress?: string;
mobileNumber?: string;
}) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
if (!emailAddress && !mobileNumber) {
throw new ApiError(400, 'Email address or mobile number is required');
}
const invitedOperator = await this.prisma.user.findFirst({
where: {
roleXid: ROLE.OPERATOR,
isActive: true,
OR: [
...(emailAddress ? [{ emailAddress }] : []),
...(mobileNumber ? [{ mobileNumber }] : []),
],
},
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
isdCode: true,
mobileNumber: true,
roleXid: true,
userPassword: true,
isEmailVerfied: true,
userStatus: true,
},
});
if (!invitedOperator) {
throw new ApiError(
403,
'Operator record not found. Please contact your host.',
);
}
return invitedOperator;
}
async signUpOperator(input: OperatorSignupInput) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
const invitedOperator = await this.findInvitedOperator({
emailAddress,
mobileNumber,
});
if (invitedOperator.userPassword) {
throw new ApiError(409, 'Operator account is already created. Please login.');
}
const otp = OtpGeneratorSixDigit.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + 5 * 60 * 1000);
await this.prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: invitedOperator.id },
data: {
firstName: input.firstName?.trim() || invitedOperator.firstName || null,
lastName: input.lastName?.trim() || invitedOperator.lastName || null,
emailAddress: emailAddress || invitedOperator.emailAddress,
isdCode: input.isdCode?.trim() || invitedOperator.isdCode || null,
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
userStatus: USER_STATUS.INVITED,
},
});
await tx.userOtp.updateMany({
where: {
userXid: invitedOperator.id,
otpType: 'Register',
isActive: true,
},
data: {
isActive: false,
isVerified: true,
},
});
await tx.userOtp.create({
data: {
userXid: invitedOperator.id,
otpType: 'Register',
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
});
return {
userId: invitedOperator.id,
emailAddress: emailAddress || invitedOperator.emailAddress,
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
otp,
expiresOn: expiry,
};
}
async verifyOperatorOtp(input: { emailAddress?: string; mobileNumber?: string; otp: string }) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
const otp = input.otp.trim();
const invitedOperator = await this.findInvitedOperator({
emailAddress,
mobileNumber,
});
const latestOtp = await this.prisma.userOtp.findFirst({
where: {
userXid: invitedOperator.id,
otpType: 'Register',
isActive: true,
isVerified: false,
},
orderBy: { createdAt: 'desc' },
});
if (!latestOtp) {
throw new ApiError(400, 'No OTP found.');
}
if (new Date() > latestOtp.expiresOn) {
throw new ApiError(400, 'OTP has expired.');
}
const isMatch = await bcrypt.compare(otp, latestOtp.otpCode);
if (!isMatch) {
throw new ApiError(400, 'Invalid OTP.');
}
await this.prisma.userOtp.update({
where: { id: latestOtp.id },
data: {
isVerified: true,
verifiedOn: new Date(),
isActive: false,
},
});
await this.prisma.user.update({
where: { id: invitedOperator.id },
data: {
isEmailVerfied: emailAddress ? true : invitedOperator.isEmailVerfied,
},
});
return invitedOperator;
}
async createOperatorPassword(userId: number, password: string) {
const user = await this.prisma.user.findFirst({
where: {
id: userId,
roleXid: ROLE.OPERATOR,
isActive: true,
},
select: {
id: true,
emailAddress: true,
userPassword: true,
},
});
if (!user) {
throw new ApiError(404, 'Operator not found');
}
if (user.userPassword) {
throw new ApiError(400, 'Password already exists. Please login.');
}
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
const hashedPassword = await bcrypt.hash(password, saltRounds);
await this.prisma.user.update({
where: { id: user.id },
data: {
userPassword: hashedPassword,
userStatus: USER_STATUS.ACTIVE,
isEmailVerfied: true,
},
});
return {
id: user.id,
emailAddress: user.emailAddress,
};
}
async loginForOperator(emailAddress: string, userPassword: string) {
const existingOperator = await this.prisma.user.findFirst({
where: {
emailAddress,
roleXid: ROLE.OPERATOR,
isActive: true,
userStatus: USER_STATUS.ACTIVE,
},
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
roleXid: true,
isActive: true,
userStatus: true,
userPassword: true,
},
});
if (!existingOperator) {
throw new ApiError(404, 'Operator not found');
}
const isPasswordMatched = await bcrypt.compare(
userPassword,
existingOperator.userPassword || '',
);
if (!isPasswordMatched) {
throw new ApiError(401, 'Invalid credentials');
}
delete existingOperator.userPassword;
return existingOperator;
}
}

View File

@@ -0,0 +1,91 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../common/utils/constants/minglar.constant';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
activity_pqq_header_xid:number
}
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { title, comments , activity_pqq_header_xid} = body;
if (!title) {
throw new ApiError(400, 'Title is required');
}
if (!comments) {
throw new ApiError(400, 'Comments are required');
}
if(!activity_pqq_header_xid){
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
}
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// Add suggestion using service
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion added successfully',
data: null,
}),
};
});

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