From 0f68b51d758f9db1ec24794351615067b311c344 Mon Sep 17 00:00:00 2001 From: Ly Tuan Kiet Date: Sun, 3 Nov 2024 22:09:59 +0700 Subject: [PATCH] finish payment strategies --- src/Order/order.schema.ts | 147 +++++++++++++++++++++++----------- src/Payos/payos.controller.ts | 8 +- src/Payos/payos.service.ts | 60 ++++++++++++-- 3 files changed, 157 insertions(+), 58 deletions(-) diff --git a/src/Order/order.schema.ts b/src/Order/order.schema.ts index 4b1a4df..1ed4004 100644 --- a/src/Order/order.schema.ts +++ b/src/Order/order.schema.ts @@ -125,61 +125,112 @@ export class OrderSchema extends PothosSchema { }), }, resolve: async (query, _root, args, ctx, _info) => { - return this.prisma.$transaction(async (prisma) => { - if (ctx.isSubscription) { - throw new Error('Subscription is not allowed') - } - if (!args.data.service.connect?.id) { - throw new Error('Service not found') - } - const order = await prisma.order.create({ - ...query, - data: { - status: OrderStatus.PENDING, - total: - (args.data.service.connect?.price as number | undefined) ?? 0, - userId: ctx.http.me.id, - serviceId: args.data.service.connect.id, - scheduleId: args.data.scheduleId, - }, + if (ctx.isSubscription) { + throw new Error('Subscription is not allowed') + } + if (!args.data.service.connect?.id) { + throw new Error('Service not found') + } + // query service + const service = await this.prisma.service.findUnique({ + where: { id: args.data.service.connect.id }, + }) + if (!service) { + throw new Error('Service not found') + } + // check if input schedule has order id then throw error + const schedule = await this.prisma.schedule.findUnique({ + where: { id: args.data.scheduleId }, + }) + if (schedule?.orderId) { + // check if order status is PAID OR PENDING + const order = await this.prisma.order.findUnique({ + where: { id: schedule.orderId }, }) - // check if service is valid - if (!args.data.service.connect) { - throw new Error('Service not found') + if ( + order?.status === OrderStatus.PAID || + order?.status === OrderStatus.PENDING + ) { + throw new Error('Schedule already has an order') } - // check if service price is free - if (args.data.service.connect.price === 0) { - return order - } - // random integer - const paymentCode = Math.floor(Math.random() * 1000000) - // create payment - const payment = await prisma.payment.create({ + } + const order = await this.prisma.order.create({ + ...query, + data: { + status: OrderStatus.PENDING, + total: service.price, + userId: ctx.http.me.id, + serviceId: service.id, + scheduleId: args.data.scheduleId, + }, + }) + // check if service is valid + if (!args.data.service.connect) { + throw new Error('Service not found') + } + + // check if order is free + if (order.total === 0) { + // assign schedule + await this.prisma.schedule.update({ + where: { id: args.data.scheduleId }, data: { orderId: order.id, - amount: args.data.service.connect.price as number, - paymentCode: paymentCode.toString(), - expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toJSDate(), - }, - }) - // generate payment url - const paymentData = await this.payosService.createPayment({ - orderCode: paymentCode, - amount: args.data.service.connect.price as number, - description: args.data.service.connect.name as string, - buyerName: ctx.http.me.name as string, - buyerEmail: ctx.http.me.email as string, - returnUrl: `${process.env.PAYOS_WEBHOOK_URL}/return`, - cancelUrl: `${process.env.PAYOS_WEBHOOK_URL}/cancel`, - }) - // update payment url - await prisma.payment.update({ - where: { id: payment.id }, - data: { - paymentCode: paymentData.paymentLinkId, }, }) return order + } + + // random integer + const paymentCode = Math.floor(Math.random() * 1000000) + // create payment + const payment = await this.prisma.payment.create({ + data: { + orderId: order.id, + amount: service.price, + paymentCode: paymentCode.toString(), + expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toJSDate(), + }, + }) + // generate payment url + const paymentData = await this.payosService.createPayment({ + orderCode: paymentCode, + amount: service.price, + description: service.name, + buyerName: ctx.http.me.name, + buyerEmail: ctx.http.me.email, + returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace( + '', + service.id, + ), + cancelUrl: `${process.env.PAYOS_RETURN_URL}`.replace( + '', + service.id, + ), + expiredAt: DateTimeUtils.now() + .plus({ minutes: 15 }) + .toUnixInteger(), + }) + // update order payment id + await this.prisma.order.update({ + where: { id: order.id }, + data: { + paymentId: payment.id, + }, + }) + // update payment url + await this.prisma.payment.update({ + where: { id: payment.id }, + data: { + paymentCode: paymentData.paymentLinkId, + }, + }) + // refetch order + return await this.prisma.order.findUnique({ + where: { id: order.id }, + include: { + payment: true, + }, }) }, }), diff --git a/src/Payos/payos.controller.ts b/src/Payos/payos.controller.ts index 1bece3a..4311bb2 100644 --- a/src/Payos/payos.controller.ts +++ b/src/Payos/payos.controller.ts @@ -10,6 +10,7 @@ import { } from '@nestjs/common' import { PayosService } from './payos.service' import { ApiTags, ApiOperation } from '@nestjs/swagger' +import { WebhookType } from '@payos/node/lib/type' @ApiTags('Payos') @Controller('payos') @@ -19,11 +20,8 @@ export class PayosController { // webhook @Post('webhook') @ApiOperation({ summary: 'Webhook for Payos' }) - async webhook( - @Body() body: any, - @Headers('x-payos-signature') signature: string, - ) { - return this.payosService.webhook(body, signature) + async webhook(@Body() body: WebhookType) { + return this.payosService.webhook(body) } // ping webhook diff --git a/src/Payos/payos.service.ts b/src/Payos/payos.service.ts index c0479f7..002910d 100644 --- a/src/Payos/payos.service.ts +++ b/src/Payos/payos.service.ts @@ -5,7 +5,12 @@ import PayOS from '@payos/node' import type { CheckoutRequestType, CheckoutResponseDataType, + WebhookType, + WebhookDataType, + CancelPaymentLinkRequestType, + DataType, } from '@payos/node/lib/type' +import { OrderStatus, PaymentStatus, ScheduleStatus } from '@prisma/client' export type CreatePaymentBody = CheckoutRequestType export type CreatePaymentResponse = CheckoutResponseDataType @Injectable() @@ -19,9 +24,51 @@ export class PayosService { return 'pong' } - async webhook(body: any, signature: string) { - Logger.log('Webhook received', body) - return body + async webhook(data: WebhookType) { + Logger.log(`Webhook received: ${JSON.stringify(data)}`) + // verify checksum + const paymentData = this.payos.verifyPaymentWebhookData(data) + if (!paymentData) { + Logger.error(`Invalid checksum: ${JSON.stringify(data)}`) + throw new Error('Invalid checksum') + } + const paymentStatus = + paymentData.code === '00' ? PaymentStatus.PAID : PaymentStatus.CANCELLED + // update payment status + const payment = await this.prisma.payment.update({ + where: { paymentCode: paymentData.paymentLinkId }, + data: { + status: paymentStatus, + }, + }) + const orderStatus = + paymentStatus === PaymentStatus.PAID + ? OrderStatus.PAID + : OrderStatus.FAILED + // update order status + await this.prisma.order.update({ + where: { id: payment.orderId }, + data: { + status: orderStatus, + }, + }) + const order = await this.prisma.order.findUnique({ + where: { id: payment.orderId }, + }) + const schedule = await this.prisma.schedule.findUnique({ + where: { id: order?.scheduleId }, + }) + // update schedule order id + await this.prisma.schedule.update({ + where: { id: schedule?.id }, + data: { + orderId: order?.id, + status: ScheduleStatus.IN_PROGRESS, + }, + }) + return { + message: 'Payment received', + } } async createPaymentURL(body: any) { @@ -36,8 +83,11 @@ export class PayosService { return await this.payos.getPaymentLinkInformation(orderId) } - async cancelPaymentURL(body: any) { - return body + async cancelPaymentURL( + orderId: string | number, + cancellationReason?: string, + ) { + return await this.payos.cancelPaymentLink(orderId, cancellationReason) } async refundPayment(body: any) {