From bc2eda749014ef5a9d14f76c336d17c80eb83631 Mon Sep 17 00:00:00 2001 From: Ly Tuan Kiet Date: Sun, 3 Nov 2024 20:28:14 +0700 Subject: [PATCH] update payment --- package-lock.json | 68 ++++++++++++++++++++++++++++++ package.json | 3 ++ src/Graphql/graphql.builder.ts | 1 - src/Order/order.module.ts | 3 +- src/Order/order.schema.ts | 47 ++++++++++++++++++--- src/Payos/payos.controller.ts | 14 ++++++ src/Payos/payos.module.ts | 17 +++++++- src/Payos/payos.service.ts | 25 +++++++++-- src/Schedule/schedule.schema.ts | 36 ++++++++++++++++ src/common/utils/datetime.utils.ts | 8 ++++ 10 files changed, 208 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1adfce..85f1285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 78bfb7e..4f97e82 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Graphql/graphql.builder.ts b/src/Graphql/graphql.builder.ts index 59aba78..a6a7c8e 100644 --- a/src/Graphql/graphql.builder.ts +++ b/src/Graphql/graphql.builder.ts @@ -152,7 +152,6 @@ export class Builder extends SchemaBuilder { this.queryType({}) this.mutationType({}) this.subscriptionType({}) - this.globalConnectionField('totalCount', (t) => t.int({ nullable: true, diff --git a/src/Order/order.module.ts b/src/Order/order.module.ts index 8b65a28..7466a0f 100644 --- a/src/Order/order.module.ts +++ b/src/Order/order.module.ts @@ -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], }) diff --git a/src/Order/order.schema.ts b/src/Order/order.schema.ts index 0b33a88..4b1a4df 100644 --- a/src/Order/order.schema.ts +++ b/src/Order/order.schema.ts @@ -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 diff --git a/src/Payos/payos.controller.ts b/src/Payos/payos.controller.ts index f9a82e0..1bece3a 100644 --- a/src/Payos/payos.controller.ts +++ b/src/Payos/payos.controller.ts @@ -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) + } } diff --git a/src/Payos/payos.module.ts b/src/Payos/payos.module.ts index 120c874..ca4d22c 100644 --- a/src/Payos/payos.module.ts +++ b/src/Payos/payos.module.ts @@ -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], }) diff --git a/src/Payos/payos.service.ts b/src/Payos/payos.service.ts index 2e5773b..c0479f7 100644 --- a/src/Payos/payos.service.ts +++ b/src/Payos/payos.service.ts @@ -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 { + return await this.payos.createPaymentLink(body) + } + + async getPaymentStatus(orderId: string | number) { + return await this.payos.getPaymentLinkInformation(orderId) } async cancelPaymentURL(body: any) { diff --git a/src/Schedule/schedule.schema.ts b/src/Schedule/schedule.schema.ts index 530ac8b..de30950 100644 --- a/src/Schedule/schedule.schema.ts +++ b/src/Schedule/schedule.schema.ts @@ -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 }, + }) + }, + }), })) } } diff --git a/src/common/utils/datetime.utils.ts b/src/common/utils/datetime.utils.ts index 5a2c9b8..14544cc 100644 --- a/src/common/utils/datetime.utils.ts +++ b/src/common/utils/datetime.utils.ts @@ -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,