- 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.
382 lines
13 KiB
TypeScript
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,
|
|
})
|
|
},
|
|
}),
|
|
}))
|
|
}
|
|
}
|