finish payment strategies

This commit is contained in:
2024-11-03 22:09:59 +07:00
parent 6b7430c5cf
commit 0f68b51d75
3 changed files with 157 additions and 58 deletions

View File

@@ -125,21 +125,42 @@ export class OrderSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
return this.prisma.$transaction(async (prisma) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Subscription is not allowed') throw new Error('Subscription is not allowed')
} }
if (!args.data.service.connect?.id) { if (!args.data.service.connect?.id) {
throw new Error('Service not found') throw new Error('Service not found')
} }
const order = await prisma.order.create({ // 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 },
})
if (
order?.status === OrderStatus.PAID ||
order?.status === OrderStatus.PENDING
) {
throw new Error('Schedule already has an order')
}
}
const order = await this.prisma.order.create({
...query, ...query,
data: { data: {
status: OrderStatus.PENDING, status: OrderStatus.PENDING,
total: total: service.price,
(args.data.service.connect?.price as number | undefined) ?? 0,
userId: ctx.http.me.id, userId: ctx.http.me.id,
serviceId: args.data.service.connect.id, serviceId: service.id,
scheduleId: args.data.scheduleId, scheduleId: args.data.scheduleId,
}, },
}) })
@@ -147,17 +168,26 @@ export class OrderSchema extends PothosSchema {
if (!args.data.service.connect) { if (!args.data.service.connect) {
throw new Error('Service not found') throw new Error('Service not found')
} }
// check if service price is free
if (args.data.service.connect.price === 0) { // 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,
},
})
return order return order
} }
// random integer // random integer
const paymentCode = Math.floor(Math.random() * 1000000) const paymentCode = Math.floor(Math.random() * 1000000)
// create payment // create payment
const payment = await prisma.payment.create({ const payment = await this.prisma.payment.create({
data: { data: {
orderId: order.id, orderId: order.id,
amount: args.data.service.connect.price as number, amount: service.price,
paymentCode: paymentCode.toString(), paymentCode: paymentCode.toString(),
expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toJSDate(), expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toJSDate(),
}, },
@@ -165,21 +195,42 @@ export class OrderSchema extends PothosSchema {
// generate payment url // generate payment url
const paymentData = await this.payosService.createPayment({ const paymentData = await this.payosService.createPayment({
orderCode: paymentCode, orderCode: paymentCode,
amount: args.data.service.connect.price as number, amount: service.price,
description: args.data.service.connect.name as string, description: service.name,
buyerName: ctx.http.me.name as string, buyerName: ctx.http.me.name,
buyerEmail: ctx.http.me.email as string, buyerEmail: ctx.http.me.email,
returnUrl: `${process.env.PAYOS_WEBHOOK_URL}/return`, returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace(
cancelUrl: `${process.env.PAYOS_WEBHOOK_URL}/cancel`, '<serviceId>',
service.id,
),
cancelUrl: `${process.env.PAYOS_RETURN_URL}`.replace(
'<serviceId>',
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 // update payment url
await prisma.payment.update({ await this.prisma.payment.update({
where: { id: payment.id }, where: { id: payment.id },
data: { data: {
paymentCode: paymentData.paymentLinkId, paymentCode: paymentData.paymentLinkId,
}, },
}) })
return order // refetch order
return await this.prisma.order.findUnique({
where: { id: order.id },
include: {
payment: true,
},
}) })
}, },
}), }),

View File

@@ -10,6 +10,7 @@ import {
} from '@nestjs/common' } from '@nestjs/common'
import { PayosService } from './payos.service' import { PayosService } from './payos.service'
import { ApiTags, ApiOperation } from '@nestjs/swagger' import { ApiTags, ApiOperation } from '@nestjs/swagger'
import { WebhookType } from '@payos/node/lib/type'
@ApiTags('Payos') @ApiTags('Payos')
@Controller('payos') @Controller('payos')
@@ -19,11 +20,8 @@ export class PayosController {
// webhook // webhook
@Post('webhook') @Post('webhook')
@ApiOperation({ summary: 'Webhook for Payos' }) @ApiOperation({ summary: 'Webhook for Payos' })
async webhook( async webhook(@Body() body: WebhookType) {
@Body() body: any, return this.payosService.webhook(body)
@Headers('x-payos-signature') signature: string,
) {
return this.payosService.webhook(body, signature)
} }
// ping webhook // ping webhook

View File

@@ -5,7 +5,12 @@ import PayOS from '@payos/node'
import type { import type {
CheckoutRequestType, CheckoutRequestType,
CheckoutResponseDataType, CheckoutResponseDataType,
WebhookType,
WebhookDataType,
CancelPaymentLinkRequestType,
DataType,
} from '@payos/node/lib/type' } from '@payos/node/lib/type'
import { OrderStatus, PaymentStatus, ScheduleStatus } from '@prisma/client'
export type CreatePaymentBody = CheckoutRequestType export type CreatePaymentBody = CheckoutRequestType
export type CreatePaymentResponse = CheckoutResponseDataType export type CreatePaymentResponse = CheckoutResponseDataType
@Injectable() @Injectable()
@@ -19,9 +24,51 @@ export class PayosService {
return 'pong' return 'pong'
} }
async webhook(body: any, signature: string) { async webhook(data: WebhookType) {
Logger.log('Webhook received', body) Logger.log(`Webhook received: ${JSON.stringify(data)}`)
return body // 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) { async createPaymentURL(body: any) {
@@ -36,8 +83,11 @@ export class PayosService {
return await this.payos.getPaymentLinkInformation(orderId) return await this.payos.getPaymentLinkInformation(orderId)
} }
async cancelPaymentURL(body: any) { async cancelPaymentURL(
return body orderId: string | number,
cancellationReason?: string,
) {
return await this.payos.cancelPaymentLink(orderId, cancellationReason)
} }
async refundPayment(body: any) { async refundPayment(body: any) {