update payment

This commit is contained in:
2024-11-03 20:28:14 +07:00
parent e8c0e0d312
commit bc2eda7490
10 changed files with 208 additions and 14 deletions

68
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@graphql-codegen/typescript-resolvers": "^4.2.1",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/apollo": "^12.2.0",
"@nestjs/axios": "^3.1.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
@@ -24,6 +25,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.2",
"@payos/node": "^1.0.10",
"@pothos/core": "^4.3.0",
"@pothos/plugin-add-graphql": "^4.1.0",
"@pothos/plugin-authz": "^3.5.10",
@@ -38,6 +40,7 @@
"@smatch-corp/nestjs-pothos": "^0.3.0",
"@smatch-corp/nestjs-pothos-apollo-driver": "^0.1.0",
"apollo-server-express": "^3.13.0",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
@@ -4284,6 +4287,17 @@
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
"license": "0BSD"
},
"node_modules/@nestjs/axios": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.1.tgz",
"integrity": "sha512-ySoxrzqX80P1q6LKLKGcgyBd2utg4gbC+4FsJNpXYvILorMlxss/ECNogD9EXLCE4JS5exVFD5ez0nK5hXcNTQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
"axios": "^1.3.1",
"rxjs": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@nestjs/cli": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz",
@@ -4928,6 +4942,16 @@
"license": "MIT",
"optional": true
},
"node_modules/@payos/node": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@payos/node/-/node-1.0.10.tgz",
"integrity": "sha512-dY+WHd6pLa558a1G8yv6oKfVe5QLTNyYnQBaSQtwvMAm/p0faKAnfXt04LNIwO9/4buas4ES+sDxc1bfX/mVbQ==",
"license": "ISC",
"dependencies": {
"axios": "^1.5.0",
"crypto": "^1.0.1"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -7163,6 +7187,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -8388,6 +8423,13 @@
"node": ">= 8"
}
},
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
"license": "ISC"
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -9816,6 +9858,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -14947,6 +15009,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pug": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",

View File

@@ -39,6 +39,7 @@
"@graphql-codegen/typescript-resolvers": "^4.2.1",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/apollo": "^12.2.0",
"@nestjs/axios": "^3.1.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
@@ -46,6 +47,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.2",
"@payos/node": "^1.0.10",
"@pothos/core": "^4.3.0",
"@pothos/plugin-add-graphql": "^4.1.0",
"@pothos/plugin-authz": "^3.5.10",
@@ -60,6 +62,7 @@
"@smatch-corp/nestjs-pothos": "^0.3.0",
"@smatch-corp/nestjs-pothos-apollo-driver": "^0.1.0",
"apollo-server-express": "^3.13.0",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",

View File

@@ -152,7 +152,6 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
this.queryType({})
this.mutationType({})
this.subscriptionType({})
this.globalConnectionField('totalCount', (t) =>
t.int({
nullable: true,

View File

@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common'
import { OrderSchema } from './order.schema'
import { PayosModule } from 'src/Payos/payos.module'
@Module({
imports: [PayosModule],
providers: [OrderSchema],
exports: [OrderSchema],
})

View File

@@ -8,11 +8,14 @@ import {
import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service'
import { OrderStatus } from '@prisma/client'
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
import { PayosService } from 'src/Payos/payos.service'
@Injectable()
export class OrderSchema extends PothosSchema {
constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService,
private readonly payosService: PayosService,
) {
super()
}
@@ -121,11 +124,24 @@ export class OrderSchema extends PothosSchema {
required: true,
}),
},
resolve: async (query, _root, args, _ctx, _info) => {
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: args.data,
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,
},
})
// check if service is valid
if (!args.data.service.connect) {
@@ -135,15 +151,32 @@ export class OrderSchema extends PothosSchema {
if (args.data.service.connect.price === 0) {
return order
}
// generate payment code by prefix 'EPESS' + 6 hex digits
const paymentCode = 'EPESS' + Math.random().toString(16).slice(2, 8)
// random integer
const paymentCode = Math.floor(Math.random() * 1000000)
// create payment
await prisma.payment.create({
const payment = await prisma.payment.create({
data: {
orderId: order.id,
amount: args.data.service.connect.price as number,
paymentCode: paymentCode,
expiredAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
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

View File

@@ -32,4 +32,18 @@ export class PayosController {
async ping() {
return this.payosService.ping()
}
// test create payment url
@Post('create-payment-url')
@ApiOperation({ summary: 'Test create payment url' })
async createPaymentURL(@Body() body: any) {
return this.payosService.createPaymentURL(body)
}
// get payment status
@Get('get-payment-status/:orderId')
@ApiOperation({ summary: 'Get payment status' })
async getPaymentStatus(@Param('orderId') orderId: string | number) {
return this.payosService.getPaymentStatus(orderId)
}
}

View File

@@ -1,9 +1,24 @@
import { Module } from '@nestjs/common'
import { PayosController } from './payos.controller'
import { PayosService } from './payos.service'
import { HttpModule } from '@nestjs/axios'
import PayOS from '@payos/node'
@Module({
providers: [PayosService],
imports: [HttpModule],
providers: [
PayosService,
{
provide: 'PayOS',
useFactory: () => {
return new PayOS(
process.env.PAYOS_CLIENT_ID ?? '',
process.env.PAYOS_API_KEY ?? '',
process.env.PAYOS_CHECKSUM_KEY ?? '',
)
},
},
],
controllers: [PayosController],
exports: [PayosService],
})

View File

@@ -1,10 +1,19 @@
import { Injectable, Logger } from '@nestjs/common'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { PrismaService } from '../Prisma/prisma.service'
import PayOS from '@payos/node'
import type {
CheckoutRequestType,
CheckoutResponseDataType,
} from '@payos/node/lib/type'
export type CreatePaymentBody = CheckoutRequestType
export type CreatePaymentResponse = CheckoutResponseDataType
@Injectable()
export class PayosService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
@Inject('PayOS') private readonly payos: PayOS,
) {}
async ping() {
return 'pong'
@@ -16,7 +25,15 @@ export class PayosService {
}
async createPaymentURL(body: any) {
return body
return await this.payos.createPaymentLink(body)
}
async createPayment(body: CreatePaymentBody): Promise<CreatePaymentResponse> {
return await this.payos.createPaymentLink(body)
}
async getPaymentStatus(orderId: string | number) {
return await this.payos.getPaymentLinkInformation(orderId)
}
async cancelPaymentURL(body: any) {

View File

@@ -72,6 +72,20 @@ export class ScheduleSchema extends PothosSchema {
})
}
@PothosRef()
scheduleConnection() {
return this.builder.simpleObject('ScheduleConnection', {
fields: (t) => ({
totalCount: t.int({
nullable: true,
}),
schedules: t.field({
type: [this.schedule()],
}),
}),
})
}
@PothosRef()
scheduleSlot() {
return this.builder.simpleObject('ScheduleSlot', {
@@ -326,6 +340,28 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1
})
},
}),
updateScheduleStatus: t.prismaField({
type: this.schedule(),
description: 'Update a schedule status.',
args: {
scheduleId: t.arg({
type: 'String',
required: true,
}),
status: t.arg({
type: ScheduleStatus,
required: true,
}),
},
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.schedule.update({
...query,
where: { id: args.scheduleId },
data: { status: args.status },
})
},
}),
}))
}
}

View File

@@ -26,6 +26,14 @@ export type TimeType = {
@Injectable()
export class DateTimeUtils {
static nowAsJSDate(): Date {
return DateTime.now().toJSDate()
}
static now(): DateTime {
return DateTime.now()
}
static getOverlapRange(
startA: DateTime,
endA: DateTime,