made the razorpay webhook api
This commit is contained in:
@@ -67,6 +67,7 @@ provider:
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
|
||||
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
|
||||
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
|
||||
|
||||
iam:
|
||||
role:
|
||||
|
||||
@@ -59,6 +59,9 @@ provider:
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
|
||||
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
|
||||
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
|
||||
|
||||
iam:
|
||||
role:
|
||||
|
||||
@@ -513,6 +513,21 @@ verifyRazorpayPayment:
|
||||
path: /payment/verify-payment
|
||||
method: post
|
||||
|
||||
razorpayWebhook:
|
||||
handler: src/modules/user/handlers/payment/razorpayWebhook.handler
|
||||
memorySize: 512
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /payment/webhook/razorpay
|
||||
method: post
|
||||
|
||||
getMatchingBucketInterestedActivities:
|
||||
handler: src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.handler
|
||||
memorySize: 512
|
||||
|
||||
@@ -87,6 +87,7 @@ const envVarsSchema = yup
|
||||
HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required'),
|
||||
RAZORPAY_KEY_SECRET: yup.string().required('Razorpay key secret is required'),
|
||||
RAZORPAY_KEY_ID: yup.string().required('Razorpay key id is required'),
|
||||
RAZORPAY_WEBHOOK_SECRET: yup.string().required('Razorpay webhook secret is required'),
|
||||
})
|
||||
.noUnknown(true);
|
||||
|
||||
@@ -169,6 +170,7 @@ function getConfig() {
|
||||
HOST_LINK_PQ: envVars.HOST_LINK_PQ,
|
||||
RAZORPAY_KEY_ID: envVars.RAZORPAY_KEY_ID,
|
||||
RAZORPAY_KEY_SECRET: envVars.RAZORPAY_KEY_SECRET,
|
||||
RAZORPAY_WEBHOOK_SECRET: envVars.RAZORPAY_WEBHOOK_SECRET,
|
||||
|
||||
// oneSignal: {
|
||||
// appID: envVars.ONESIGNAL_APPID,
|
||||
|
||||
63
src/modules/user/handlers/payment/razorpayWebhook.ts
Normal file
63
src/modules/user/handlers/payment/razorpayWebhook.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { PaymentService } from '../../services/payment.service';
|
||||
|
||||
const paymentService = new PaymentService(prismaClient);
|
||||
|
||||
function getHeaderValue(
|
||||
headers: APIGatewayProxyEvent['headers'],
|
||||
name: string,
|
||||
): string | undefined {
|
||||
const targetName = name.toLowerCase();
|
||||
|
||||
for (const [key, value] of Object.entries(headers ?? {})) {
|
||||
if (key.toLowerCase() === targetName) {
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const signature =
|
||||
getHeaderValue(event.headers, 'x-razorpay-signature')?.trim() ?? '';
|
||||
|
||||
if (!signature) {
|
||||
throw new ApiError(400, 'Missing Razorpay webhook signature.');
|
||||
}
|
||||
|
||||
const rawBody = event.body
|
||||
? event.isBase64Encoded
|
||||
? Buffer.from(event.body, 'base64').toString('utf8')
|
||||
: event.body
|
||||
: '';
|
||||
|
||||
const result = await paymentService.handleWebhook({
|
||||
rawBody,
|
||||
signature,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Webhook received',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -11,10 +11,42 @@ const razorpay = new Razorpay({
|
||||
key_secret: config.RAZORPAY_KEY_SECRET,
|
||||
});
|
||||
|
||||
type RazorpayWebhookPayload = {
|
||||
event?: string;
|
||||
payload?: {
|
||||
payment?: {
|
||||
entity?: {
|
||||
id?: string;
|
||||
order_id?: string;
|
||||
status?: string;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
};
|
||||
};
|
||||
order?: {
|
||||
entity?: {
|
||||
id?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
private signaturesMatch(expectedSignature: string, receivedSignature: string) {
|
||||
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
|
||||
const receivedBuffer = Buffer.from(receivedSignature, 'utf8');
|
||||
|
||||
if (expectedBuffer.length !== receivedBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
|
||||
}
|
||||
|
||||
async createOrder(
|
||||
userXid: number,
|
||||
payload: {
|
||||
@@ -175,4 +207,156 @@ export class PaymentService {
|
||||
paidAt: updatedPaymentOrder.paidAt,
|
||||
};
|
||||
}
|
||||
|
||||
async handleWebhook(payload: {
|
||||
rawBody: string;
|
||||
signature: string;
|
||||
}) {
|
||||
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET?.trim();
|
||||
|
||||
if (!webhookSecret) {
|
||||
throw new ApiError(
|
||||
500,
|
||||
'Razorpay webhook secret is not configured.',
|
||||
);
|
||||
}
|
||||
|
||||
const rawBody = payload.rawBody;
|
||||
const signature = payload.signature?.trim();
|
||||
|
||||
if (!rawBody) {
|
||||
throw new ApiError(400, 'Webhook body is required.');
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
throw new ApiError(400, 'Razorpay webhook signature is required.');
|
||||
}
|
||||
|
||||
const generatedSignature = crypto
|
||||
.createHmac('sha256', webhookSecret)
|
||||
.update(rawBody, 'utf8')
|
||||
.digest('hex');
|
||||
|
||||
if (!this.signaturesMatch(generatedSignature, signature)) {
|
||||
throw new ApiError(400, 'Invalid webhook signature.');
|
||||
}
|
||||
|
||||
let body: RazorpayWebhookPayload;
|
||||
try {
|
||||
body = JSON.parse(rawBody) as RazorpayWebhookPayload;
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid webhook JSON body.');
|
||||
}
|
||||
|
||||
const eventType = body.event?.trim();
|
||||
if (!eventType) {
|
||||
throw new ApiError(400, 'Webhook event type is missing.');
|
||||
}
|
||||
|
||||
const paymentEntity = body.payload?.payment?.entity;
|
||||
const orderEntity = body.payload?.order?.entity;
|
||||
const razorpayOrderId =
|
||||
paymentEntity?.order_id?.trim() || orderEntity?.id?.trim();
|
||||
const razorpayPaymentId = paymentEntity?.id?.trim();
|
||||
|
||||
if (!razorpayOrderId) {
|
||||
throw new ApiError(400, 'Webhook payload is missing Razorpay order id.');
|
||||
}
|
||||
|
||||
const paymentOrder = await this.prisma.paymentOrders.findFirst({
|
||||
where: {
|
||||
razorpayOrderId,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!paymentOrder) {
|
||||
return {
|
||||
eventType,
|
||||
processed: false,
|
||||
message: 'Payment order not found for webhook payload.',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
eventType === 'payment.captured' &&
|
||||
paymentOrder.paymentStatus === 'paid' &&
|
||||
paymentOrder.razorpayPaymentId === razorpayPaymentId
|
||||
) {
|
||||
return {
|
||||
paymentOrderId: paymentOrder.id,
|
||||
orderId: paymentOrder.razorpayOrderId,
|
||||
paymentId: paymentOrder.razorpayPaymentId,
|
||||
status: paymentOrder.paymentStatus,
|
||||
verifiedAt: paymentOrder.verifiedAt,
|
||||
paidAt: paymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType === 'payment.captured' || eventType === 'order.paid') {
|
||||
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
},
|
||||
data: {
|
||||
...(razorpayPaymentId
|
||||
? { razorpayPaymentId }
|
||||
: {}),
|
||||
paymentStatus: 'paid',
|
||||
verifiedAt: new Date(),
|
||||
paidAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
paymentOrderId: updatedPaymentOrder.id,
|
||||
orderId: updatedPaymentOrder.razorpayOrderId,
|
||||
paymentId: updatedPaymentOrder.razorpayPaymentId,
|
||||
status: updatedPaymentOrder.paymentStatus,
|
||||
verifiedAt: updatedPaymentOrder.verifiedAt,
|
||||
paidAt: updatedPaymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType === 'payment.failed') {
|
||||
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
|
||||
where: {
|
||||
id: paymentOrder.id,
|
||||
},
|
||||
data: {
|
||||
...(razorpayPaymentId
|
||||
? { razorpayPaymentId }
|
||||
: {}),
|
||||
paymentStatus: 'failed',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
paymentOrderId: updatedPaymentOrder.id,
|
||||
orderId: updatedPaymentOrder.razorpayOrderId,
|
||||
paymentId: updatedPaymentOrder.razorpayPaymentId,
|
||||
status: updatedPaymentOrder.paymentStatus,
|
||||
verifiedAt: updatedPaymentOrder.verifiedAt,
|
||||
paidAt: updatedPaymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
paymentOrderId: paymentOrder.id,
|
||||
orderId: paymentOrder.razorpayOrderId,
|
||||
paymentId: paymentOrder.razorpayPaymentId,
|
||||
status: paymentOrder.paymentStatus,
|
||||
verifiedAt: paymentOrder.verifiedAt,
|
||||
paidAt: paymentOrder.paidAt,
|
||||
eventType,
|
||||
processed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user