3 Commits

Author SHA1 Message Date
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
5 changed files with 300 additions and 1 deletions

View File

@@ -1779,6 +1779,7 @@ model ItineraryHeader {
toDate DateTime @map("to_date") toDate DateTime @map("to_date")
toTime String @map("to_time") @db.VarChar(30) toTime String @map("to_time") @db.VarChar(30)
itineraryStatus String @default("draft") @map("itinerary_status") @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") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")

View File

@@ -498,6 +498,21 @@ getAllUserSavedItineraries:
path: /itinerary/get-all-user-saved-itineraries path: /itinerary/get-all-user-saved-itineraries
method: get 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: createRazorpayOrder:
handler: src/modules/user/handlers/payment/createOrder.handler handler: src/modules/user/handlers/payment/createOrder.handler
memorySize: 512 memorySize: 512

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( async bookItineraryAfterPayment(
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
userXid: number, userXid: number,

View File

@@ -12,6 +12,17 @@ const razorpay = new Razorpay({
key_secret: config.RAZORPAY_KEY_SECRET, 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 = { type RazorpayWebhookPayload = {
event?: string; event?: string;
payload?: { payload?: {
@@ -91,7 +102,7 @@ export class PaymentService {
} }
const amountInPaise = Math.round(payload.amount * 100); 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 currency = payload.currency ?? 'INR';
const order = (await razorpay.orders.create({ const order = (await razorpay.orders.create({