Files
epess-web-backend/src/Order/order.schema.ts
Ly Tuan Kiet b709bec583 feat: enhance CronService and OrderSchema with new scheduling and authorization features
- Removed unnecessary logging from CronService methods to streamline execution.
- Introduced a new cron job in CronService to check if all schedule dates are completed and update the schedule status accordingly.
- Added a new query in OrderSchema to retrieve completed orders for moderators, including authorization checks for user roles.
- Updated PersonalMilestoneSchema to allow for creating multiple personal milestones with necessary input fields.

These changes improve the scheduling logic and enhance the GraphQL API's authorization mechanisms, ensuring better user experience and state management.
2024-12-13 20:14:18 +07:00

382 lines
13 KiB
TypeScript

import { Inject, Injectable, Logger } from '@nestjs/common'
import { OrderStatus, Role, ScheduleDateStatus, ScheduleStatus } from '@prisma/client'
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
import _ from 'lodash'
import { Builder } from '../Graphql/graphql.builder'
import { PayosService } from '../Payos/payos.service'
import { PrismaService } from '../Prisma/prisma.service'
import { DateTimeUtils } from '../common/utils/datetime.utils'
@Injectable()
export class OrderSchema extends PothosSchema {
constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService,
private readonly payosService: PayosService,
) {
super()
}
// Types section
@PothosRef()
order() {
return this.builder.prismaObject('Order', {
description: 'An order in the system.',
fields: (t) => ({
id: t.exposeID('id', {
description: 'The ID of the order.',
}),
userId: t.exposeID('userId', {
description: 'The ID of the user.',
}),
serviceId: t.exposeID('serviceId', {
description: 'The ID of the service.',
}),
status: t.expose('status', {
type: OrderStatus,
description: 'The status of the order.',
}),
total: t.exposeInt('total', {
description: 'The total price of the order.',
}),
scheduleId: t.exposeID('scheduleId', {
description: 'The ID of the schedule.',
}),
schedule: t.relation('schedule', {
description: 'The schedule of the order.',
}),
disbursed: t.exposeBoolean('disbursed', {
description: 'Whether the order has been disbursed.',
}),
chatRoomId: t.exposeID('chatRoomId', {
description: 'The ID of the chat room.',
}),
chatRoom: t.relation('chatRoom', {
description: 'The chat room of the order.',
}),
createdAt: t.expose('createdAt', {
type: 'DateTime',
description: 'The date and time the order was created.',
}),
updatedAt: t.expose('updatedAt', {
type: 'DateTime',
description: 'The date and time the order was updated.',
}),
commission: t.exposeFloat('commission', {
description: 'The commission of the order.',
}),
user: t.relation('user', {
description: 'The user who made the order.',
}),
service: t.relation('service', {
description: 'The service for the order.',
}),
refundTicket: t.relation('refundTicket', {
description: 'The refund ticket for the order.',
}),
payment: t.relation('payment', {
description: 'The payment for the order.',
}),
paymentId: t.exposeString('paymentId', {
description: 'The ID of the payment.',
}),
}),
})
}
@Pothos()
init(): void {
// query section
this.builder.queryFields((t) => ({
orders: t.prismaField({
type: [this.order()],
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({
...query,
take: args.take ?? undefined,
skip: args.skip ?? undefined,
orderBy: args.orderBy ?? undefined,
where: args.filter ?? undefined,
})
},
}),
order: t.prismaField({
type: this.order(),
args: this.builder.generator.findUniqueArgs('Order'),
description: 'Retrieve a single order by its unique identifier.',
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.order.findUnique({
...query,
where: args.where,
})
},
}),
completedOrders: t.prismaField({
type: [this.order()],
description: 'Retrieve a list of completed orders',
args: this.builder.generator.findManyArgs('Order'),
resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Orders cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('Unauthorized')
}
// return orders where user is the one who made the order and status is PAID and schedule.dates is in the past
return await this.prisma.order.findMany({
...query,
where: {
AND: [
...(args.filter ? [args.filter] : []),
{
userId: ctx.http.me.id,
status: OrderStatus.PAID,
schedule: {
OR: [
{
dates: {
every: {
OR: [
{
end: { lte: DateTimeUtils.now().toJSDate() },
},
{
status: ScheduleDateStatus.COMPLETED,
},
],
},
},
},
// or cancelled
{
status: ScheduleStatus.REFUNDED,
},
],
},
},
],
},
})
},
}),
completedOrdersForModerator: t.prismaField({
type: [this.order()],
description: 'Retrieve a list of completed orders for moderator',
args: this.builder.generator.findManyArgs('Order'),
resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Orders cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('Unauthorized')
}
// only for role moderator
if (ctx.http.me.role !== Role.MODERATOR) {
throw new Error('Unauthorized')
}
// return completed order list where schedule status is COMPLETED
return await this.prisma.order.findMany({
...query,
where: {
AND: [
...(args.filter ? [args.filter] : []),
{
status: OrderStatus.PAID,
schedule: {
status: ScheduleStatus.COMPLETED,
},
},
],
},
})
},
}),
}))
// mutation section
this.builder.mutationFields((t) => ({
createOrder: t.prismaField({
type: this.order(),
description: 'Create a new order.',
args: {
data: t.arg({
type: this.builder.generator.getCreateInput('Order', [
'id',
'user',
'paymentId',
'payment',
'refundTicket',
'status',
'total',
'createdAt',
'updatedAt',
'commission',
]),
required: true,
}),
},
resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized')
}
if (!args.data.service.connect?.id) {
throw new Error('Service not found')
}
// 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 user has already registered for this service
const userService = await this.prisma.schedule.findFirst({
where: {
AND: [
{
customerId: ctx.http.me?.id,
},
{
managedService: {
serviceId: service.id,
},
},
],
},
})
if (userService) {
throw new Error('User has already registered for this service')
}
// check if input schedule has order id then throw error
const schedule = await this.prisma.schedule.findUnique({
where: { id: args.data.schedule.connect?.id ?? '' },
})
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,
data: {
status: OrderStatus.PENDING,
total: service.price,
userId: ctx.http.me?.id ?? '',
serviceId: service.id,
scheduleId: args.data.schedule.connect?.id ?? '',
commission: service.commission ?? 0.0,
},
})
// 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.schedule.connect?.id ?? '' },
data: {
orderId: order.id,
},
})
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(),
},
})
const _name = _.deburr(service.name).slice(0, 10)
Logger.log(`Creating payment for ${_name}`)
// generate payment url
const paymentData = await this.payosService.createPayment({
orderCode: paymentCode,
amount: service.price,
description: _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,
},
})
},
}),
deleteOrder: t.prismaField({
type: this.order(),
description: 'Delete an existing order.',
args: {
where: t.arg({
type: this.builder.generator.getWhereUnique('Order'),
required: true,
}),
},
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.order.delete({
...query,
where: args.where,
})
},
}),
updateOrder: t.prismaField({
type: this.order(),
description: 'Update an existing order.',
args: {
data: t.arg({
type: this.builder.generator.getUpdateInput('Order', ['status', 'total']),
required: true,
}),
where: t.arg({
type: this.builder.generator.getWhereUnique('Order'),
required: true,
}),
},
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.order.update({
...query,
data: args.data,
where: args.where,
})
},
}),
}))
}
}