diff --git a/src/Finance/finance.module.ts b/src/Finance/finance.module.ts new file mode 100644 index 0000000..4064d8d --- /dev/null +++ b/src/Finance/finance.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common' +import { FinanceSchema } from './finance.schema' + +@Module({ + providers: [FinanceSchema], + exports: [FinanceSchema], +}) +export class FinanceModule {} diff --git a/src/Finance/finance.schema.ts b/src/Finance/finance.schema.ts new file mode 100644 index 0000000..9bc03a3 --- /dev/null +++ b/src/Finance/finance.schema.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common' +import { Pothos, PothosRef } from '@smatch-corp/nestjs-pothos' + +@Injectable() +export class FinanceSchema {} diff --git a/src/Order/order.schema.ts b/src/Order/order.schema.ts index 367fcfe..b5128e7 100644 --- a/src/Order/order.schema.ts +++ b/src/Order/order.schema.ts @@ -1,10 +1,5 @@ import { Inject, Injectable } from '@nestjs/common' -import { - Pothos, - PothosRef, - PothosSchema, - SchemaBuilderToken, -} from '@smatch-corp/nestjs-pothos' +import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { Builder } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' import { OrderStatus } from '@prisma/client' @@ -81,8 +76,7 @@ export class OrderSchema extends PothosSchema { this.builder.queryFields((t) => ({ orders: t.prismaField({ type: [this.order()], - description: - 'Retrieve a list of orders with optional filtering, ordering, and pagination.', + description: 'Retrieve a list of orders with optional filtering, ordering, and pagination.', args: this.builder.generator.findManyArgs('Order'), resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.order.findMany({ @@ -113,17 +107,7 @@ export class OrderSchema extends PothosSchema { description: 'Create a new order.', args: { data: t.arg({ - type: this.builder.generator.getCreateInput('Order', [ - 'id', - 'user', - 'paymentId', - 'payment', - 'refundTicket', - 'status', - 'total', - 'createdAt', - 'updatedAt', - ]), + type: this.builder.generator.getCreateInput('Order', ['id', 'user', 'paymentId', 'payment', 'refundTicket', 'status', 'total', 'createdAt', 'updatedAt']), required: true, }), }, @@ -150,10 +134,7 @@ export class OrderSchema extends PothosSchema { const order = await this.prisma.order.findUnique({ where: { id: schedule.orderId }, }) - if ( - order?.status === OrderStatus.PAID || - order?.status === OrderStatus.PENDING - ) { + if (order?.status === OrderStatus.PAID || order?.status === OrderStatus.PENDING) { throw new Error('Schedule already has an order') } } @@ -202,17 +183,9 @@ export class OrderSchema extends PothosSchema { 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(), + 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({ @@ -230,12 +203,12 @@ export class OrderSchema extends PothosSchema { }) // update orderId for schedule dates - await this.prisma.scheduleDate.updateMany({ - where: { scheduleId: args.data.schedule.connect?.id ?? '' }, - data: { - orderId: order.id, - }, - }) + // await this.prisma.scheduleDate.updateMany({ + // where: { scheduleId: args.data.schedule.connect?.id ?? '' }, + // data: { + // orderId: order.id, + // }, + // }) // refetch order return await this.prisma.order.findUnique({ @@ -267,10 +240,7 @@ export class OrderSchema extends PothosSchema { description: 'Update an existing order.', args: { data: t.arg({ - type: this.builder.generator.getUpdateInput('Order', [ - 'status', - 'total', - ]), + type: this.builder.generator.getUpdateInput('Order', ['status', 'total']), required: true, }), where: t.arg({ diff --git a/src/Payos/payos.service.ts b/src/Payos/payos.service.ts index e7f015a..5bb274f 100644 --- a/src/Payos/payos.service.ts +++ b/src/Payos/payos.service.ts @@ -2,20 +2,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common' import { PrismaService } from '../Prisma/prisma.service' import PayOS from '@payos/node' -import type { - CheckoutRequestType, - CheckoutResponseDataType, - WebhookType, - WebhookDataType, - CancelPaymentLinkRequestType, - DataType, -} from '@payos/node/lib/type' -import { - ChatRoomType, - OrderStatus, - PaymentStatus, - ScheduleStatus, -} from '@prisma/client' +import type { CheckoutRequestType, CheckoutResponseDataType, WebhookType, WebhookDataType, CancelPaymentLinkRequestType, DataType } from '@payos/node/lib/type' +import { ChatRoomType, OrderStatus, PaymentStatus, ScheduleStatus } from '@prisma/client' export type CreatePaymentBody = CheckoutRequestType export type CreatePaymentResponse = CheckoutResponseDataType @Injectable() @@ -31,74 +19,87 @@ export class PayosService { async webhook(data: WebhookType) { Logger.log(`Webhook received: ${JSON.stringify(data)}`) - // check if orderCode = 123 mean it's a test payment and auto response success + + /* -------------------------------------------------------------------------- */ + /* Test payment */ + /* -------------------------------------------------------------------------- */ if (data.data.orderCode === 123) { return { message: 'Payment received', } } - // 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.findUniqueOrThrow({ - 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: { - customerId: order?.userId, - orderId: order?.id, - status: ScheduleStatus.IN_PROGRESS, - }, - }) - // get mentor id from managed service - const managedService = await this.prisma.managedService.findUniqueOrThrow({ - where: { id: schedule?.managedServiceId }, - }) - const mentorId = managedService.mentorId - // get center id from order service - const orderService = await this.prisma.service.findUniqueOrThrow({ - where: { id: order?.serviceId }, - }) - const centerId = orderService.centerId - // create chatroom for service meeting room - await this.prisma.chatRoom.create({ - data: { - type: ChatRoomType.SUPPORT, - customerId: order.userId, - centerId: centerId, - mentorId: mentorId, - }, - }) - return { - message: 'Payment received', + const paymentStatus = paymentData.code === '00' ? PaymentStatus.PAID : PaymentStatus.CANCELLED + /* ---------------------------- begin transaction --------------------------- */ + try { + await this.prisma.$transaction(async (tx) => { + // update payment status + const payment = await tx.payment.update({ + where: { paymentCode: paymentData.paymentLinkId }, + data: { + status: paymentStatus, + }, + }) + const orderStatus = paymentStatus === PaymentStatus.PAID ? OrderStatus.PAID : OrderStatus.FAILED + // update order status + await tx.order.update({ + where: { id: payment.orderId }, + data: { + status: orderStatus, + }, + }) + const order = await tx.order.findUniqueOrThrow({ + where: { id: payment.orderId }, + }) + const schedule = await tx.schedule.findUnique({ + where: { id: order?.scheduleId }, + }) + // update schedule order id + await tx.schedule.update({ + where: { id: schedule?.id }, + data: { + customerId: order?.userId, + orderId: order?.id, + status: ScheduleStatus.IN_PROGRESS, + }, + }) + // get mentor id from managed service + const managedService = await tx.managedService.findUniqueOrThrow({ + where: { id: schedule?.managedServiceId }, + }) + const mentorId = managedService.mentorId + // get center id from order service + const orderService = await tx.service.findUniqueOrThrow({ + where: { id: order?.serviceId }, + }) + const centerId = orderService.centerId + // create chatroom for support + await tx.chatRoom.create({ + data: { + type: ChatRoomType.SUPPORT, + customerId: order.userId, + centerId: centerId, + mentorId: mentorId, + }, + }) + // update orderId for schedule dates + await tx.scheduleDate.updateMany({ + where: { scheduleId: schedule?.id }, + data: { + orderId: order.id, + }, + }) + return { + message: 'Payment received', + } + }) + } catch (error) { + Logger.error(`Transaction failed: ${error}`) + throw error } } @@ -114,10 +115,7 @@ export class PayosService { return await this.payos.getPaymentLinkInformation(orderId) } - async cancelPaymentURL( - orderId: string | number, - cancellationReason?: string, - ) { + async cancelPaymentURL(orderId: string | number, cancellationReason?: string) { return await this.payos.cancelPaymentLink(orderId, cancellationReason) } diff --git a/src/User/user.schema.ts b/src/User/user.schema.ts index ee6b81f..5ea554d 100644 --- a/src/User/user.schema.ts +++ b/src/User/user.schema.ts @@ -384,15 +384,16 @@ export class UserSchema extends PothosSchema { email: t.arg({ type: 'String', required: true }), }, resolve: async (_parent, args, ctx) => { + // check context + if (ctx.isSubscription) { + throw new Error('Not allowed') + } + // check context is admin + + if (ctx.http.me?.role !== 'ADMIN') { + throw new UnauthorizedException(`Only admin can invite moderator`) + } return this.prisma.$transaction(async (tx) => { - // check context - if (ctx.isSubscription) { - throw new Error('Not allowed') - } - // check context is admin - if (ctx.http.me?.role !== 'ADMIN') { - throw new UnauthorizedException(`Only admin can invite moderator`) - } let user // perform update role try {