- Updated biome.json to include "graphql.d.ts" in the ignored files list. - Updated subproject commit reference in epess-database to the latest version. - Removed unused script from package.json and streamlined module file extensions in tsconfig.json. - Consolidated exclude patterns in tsconfig.build.json for clarity. - Refactored imports across multiple schema files for consistency and improved readability. - Enhanced various schema files by ensuring proper import order and removing redundant code. - Improved error handling and data integrity checks in several service and schema files.
224 lines
8.4 KiB
TypeScript
224 lines
8.4 KiB
TypeScript
import { Inject, Injectable } from '@nestjs/common'
|
|
import { OrderStatus, PaymentStatus, RefundTicketStatus, Role } from '@prisma/client'
|
|
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
|
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
|
|
import { Builder } from '../Graphql/graphql.builder'
|
|
import { PrismaService } from '../Prisma/prisma.service'
|
|
import * as banks from '../common/utils/banks.json'
|
|
@Injectable()
|
|
export class RefundTicketSchema extends PothosSchema {
|
|
constructor(
|
|
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
|
private readonly prisma: PrismaService,
|
|
) {
|
|
super()
|
|
}
|
|
|
|
// Types section
|
|
@PothosRef()
|
|
refundTicket() {
|
|
return this.builder.prismaObject('RefundTicket', {
|
|
fields: (t) => ({
|
|
id: t.exposeID('id', {
|
|
description: 'The ID of the refund ticket.',
|
|
}),
|
|
amount: t.exposeFloat('amount', {
|
|
description: 'The amount of the refund ticket.',
|
|
}),
|
|
status: t.expose('status', {
|
|
type: RefundTicketStatus,
|
|
description: 'The status of the refund ticket.',
|
|
}),
|
|
bankBin: t.exposeString('bankBin', {
|
|
description: 'The bank BIN of the refund ticket.',
|
|
}),
|
|
bankAccountNumber: t.exposeString('bankAccountNumber', {
|
|
description: 'The bank account number of the refund ticket.',
|
|
}),
|
|
bankName: t.exposeString('bankName', {
|
|
description: 'The bank name of the refund ticket.',
|
|
}),
|
|
reason: t.exposeString('reason', {
|
|
description: 'The reason for the refund ticket.',
|
|
}),
|
|
moderator: t.relation('moderator', {
|
|
description: 'The moderator who processed the refund ticket.',
|
|
}),
|
|
createdAt: t.expose('createdAt', {
|
|
type: 'DateTime',
|
|
description: 'The date and time the refund ticket was created.',
|
|
}),
|
|
updatedAt: t.expose('updatedAt', {
|
|
type: 'DateTime',
|
|
description: 'The date and time the refund ticket was updated.',
|
|
}),
|
|
order: t.relation('order', {
|
|
description: 'The order for the refund ticket.',
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@PothosRef()
|
|
refundTicketAction() {
|
|
return this.builder.enumType('RefundTicketAction', {
|
|
description: 'The action to take on a refund ticket.',
|
|
values: ['APPROVE', 'REJECT'],
|
|
})
|
|
}
|
|
|
|
// Queries section
|
|
@Pothos()
|
|
init(): void {
|
|
this.builder.queryFields((t) => ({
|
|
refundTicket: t.prismaField({
|
|
type: this.refundTicket(),
|
|
description: 'Retrieve a refund ticket by ID.',
|
|
args: {
|
|
id: t.arg({ type: 'String', required: true }),
|
|
},
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Subscription is not allowed')
|
|
}
|
|
if (ctx.http.me?.role !== Role.MODERATOR) {
|
|
throw new Error('Only moderators can retrieve refund tickets')
|
|
}
|
|
return await this.prisma.refundTicket.findUnique({ ...query, where: { id: args.id } })
|
|
},
|
|
}),
|
|
refundTickets: t.prismaField({
|
|
type: [this.refundTicket()],
|
|
description: 'Retrieve a list of refund tickets with optional filtering, ordering, and pagination.',
|
|
args: this.builder.generator.findManyArgs('RefundTicket'),
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.refundTicket.findMany({
|
|
...query,
|
|
where: args.filter ?? undefined,
|
|
orderBy: args.orderBy ?? undefined,
|
|
cursor: args.cursor ?? undefined,
|
|
take: args.take ?? undefined,
|
|
skip: args.skip ?? undefined,
|
|
})
|
|
},
|
|
}),
|
|
}))
|
|
this.builder.mutationFields((t) => ({
|
|
requestRefund: t.prismaField({
|
|
type: this.refundTicket(),
|
|
description: 'Request a refund for an order.',
|
|
args: {
|
|
orderId: t.arg({
|
|
type: 'String',
|
|
required: true,
|
|
}),
|
|
reason: t.arg({
|
|
type: 'String',
|
|
description: 'The reason for the refund, required if the refund is rejected',
|
|
required: false,
|
|
}),
|
|
},
|
|
resolve: async (_query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Subscription is not allowed')
|
|
}
|
|
if (ctx.http.me?.role !== Role.CUSTOMER) {
|
|
throw new Error('Only customers can request refund')
|
|
}
|
|
// check if bank bin and bank account number is exists else throw error
|
|
if (!ctx.http.me?.bankBin || !ctx.http.me?.bankAccountNumber) {
|
|
throw new Error('Bank bin and bank account number are required, please update your profile first')
|
|
}
|
|
// check if order exists
|
|
const order = await this.prisma.order.findUnique({
|
|
where: { id: args.orderId },
|
|
include: {
|
|
refundTicket: true,
|
|
},
|
|
})
|
|
if (!order) {
|
|
throw new Error('Order not found')
|
|
}
|
|
// check if order status is PAID
|
|
if (order.status !== OrderStatus.PAID) {
|
|
throw new Error('Order is not paid')
|
|
}
|
|
// check if order total is not null
|
|
if (!order.total || order.total === 0) {
|
|
throw new Error('Order total is null or free')
|
|
}
|
|
if (order.refundTicket) {
|
|
throw new Error('Refund ticket already exists')
|
|
}
|
|
// calculate refund amount based on order time: if order is less than 24 hours, refund 100%, if more than 24 hours, less than 48 hours, refund 50%, if more than 72 hours, cannot refund
|
|
const now = DateTimeUtils.now()
|
|
const orderDate = DateTimeUtils.fromDate(order.createdAt)
|
|
const diffTime = Math.abs(now.diff(orderDate).toMillis())
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
let refundAmount = 0
|
|
if (diffDays < 1) refundAmount = order.total
|
|
else if (diffDays < 3) refundAmount = order.total * 0.5
|
|
if (refundAmount === 0) throw new Error('Cannot refund after 3 days')
|
|
// create refund ticket
|
|
// get bank name from bank bin from banks.json
|
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
const bank = banks.data.find((bank: any) => bank.bin === ctx.http.me?.bankBin)
|
|
if (!bank) throw new Error('Bank not found')
|
|
const refundTicket = await this.prisma.refundTicket.create({
|
|
data: {
|
|
orderId: order.id,
|
|
status: RefundTicketStatus.PENDING,
|
|
amount: refundAmount,
|
|
reason: args.reason,
|
|
bankBin: ctx.http.me?.bankBin,
|
|
bankAccountNumber: ctx.http.me?.bankAccountNumber,
|
|
bankName: bank.name,
|
|
},
|
|
})
|
|
return refundTicket
|
|
},
|
|
}),
|
|
processRefundTicket: t.prismaField({
|
|
type: this.refundTicket(),
|
|
description: 'Process a refund ticket, can only done by moderator',
|
|
args: {
|
|
refundTicketId: t.arg({
|
|
type: 'String',
|
|
required: true,
|
|
}),
|
|
action: t.arg({
|
|
type: this.refundTicketAction(),
|
|
required: true,
|
|
}),
|
|
reason: t.arg({
|
|
type: 'String',
|
|
required: false,
|
|
}),
|
|
},
|
|
resolve: async (_query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Subscription is not allowed')
|
|
}
|
|
if (ctx.http.me?.role !== Role.MODERATOR) {
|
|
throw new Error('Only moderators can process refund tickets')
|
|
}
|
|
// if action is REJECT, reason is required
|
|
if (args.action === 'REJECT' && !args.reason) {
|
|
throw new Error('Reason is required when rejecting a refund ticket')
|
|
}
|
|
// update refund ticket status
|
|
const refundTicket = await this.prisma.refundTicket.update({
|
|
where: { id: args.refundTicketId },
|
|
data: {
|
|
status: args.action === 'APPROVE' ? RefundTicketStatus.APPROVED : RefundTicketStatus.REJECTED,
|
|
rejectedReason: args.action === 'REJECT' ? args.reason : undefined,
|
|
moderatorId: ctx.http.me?.id,
|
|
},
|
|
})
|
|
return refundTicket
|
|
},
|
|
}),
|
|
}))
|
|
}
|
|
}
|