finish payment strategies
This commit is contained in:
@@ -125,61 +125,112 @@ 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')
|
}
|
||||||
}
|
// query service
|
||||||
const order = await prisma.order.create({
|
const service = await this.prisma.service.findUnique({
|
||||||
...query,
|
where: { id: args.data.service.connect.id },
|
||||||
data: {
|
})
|
||||||
status: OrderStatus.PENDING,
|
if (!service) {
|
||||||
total:
|
throw new Error('Service not found')
|
||||||
(args.data.service.connect?.price as number | undefined) ?? 0,
|
}
|
||||||
userId: ctx.http.me.id,
|
// check if input schedule has order id then throw error
|
||||||
serviceId: args.data.service.connect.id,
|
const schedule = await this.prisma.schedule.findUnique({
|
||||||
scheduleId: args.data.scheduleId,
|
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 (
|
||||||
if (!args.data.service.connect) {
|
order?.status === OrderStatus.PAID ||
|
||||||
throw new Error('Service not found')
|
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) {
|
const order = await this.prisma.order.create({
|
||||||
return order
|
...query,
|
||||||
}
|
data: {
|
||||||
// random integer
|
status: OrderStatus.PENDING,
|
||||||
const paymentCode = Math.floor(Math.random() * 1000000)
|
total: service.price,
|
||||||
// create payment
|
userId: ctx.http.me.id,
|
||||||
const payment = await prisma.payment.create({
|
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: {
|
data: {
|
||||||
orderId: order.id,
|
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
|
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(
|
||||||
|
'<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
|
||||||
|
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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user