import { Injectable, Logger } from '@nestjs/common' import { Cron } from '@nestjs/schedule' import { CronExpression } from '@nestjs/schedule' import { OrderStatus, PaymentStatus, ScheduleDateStatus, ScheduleStatus, ServiceStatus } from '@prisma/client' import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { NotificationService } from 'src/Notification/notification.service' import { PrismaService } from 'src/Prisma/prisma.service' @Injectable() export class CronService { constructor( private readonly prisma: PrismaService, private readonly notificationService: NotificationService, ) {} // cron every 1 minute to check schedule date status @Cron(CronExpression.EVERY_MINUTE) async checkScheduleDateStatus() { Logger.log('Checking schedule date status', 'checkScheduleDateStatus') const schedules = await this.prisma.scheduleDate.findMany({ where: { end: { lt: DateTimeUtils.now().toJSDate(), }, status: { notIn: [ScheduleDateStatus.COMPLETED, ScheduleDateStatus.MISSING_MENTOR, ScheduleDateStatus.MISSING_CUSTOMER], }, }, }) Logger.log(`Found ${schedules.length} schedules to update`, 'checkScheduleDateStatus') // get all collaboration sessions const collaborationSessions = await this.prisma.collaborationSession.findMany({ where: { collaboratorsIds: { hasEvery: schedules.map((s) => s.id), }, }, }) const updates = schedules .map((schedule) => { const collaborationSession = collaborationSessions.find((session) => session.scheduleDateId === schedule.id) if (!collaborationSession) { return { id: schedule.id, status: ScheduleDateStatus.MISSING_MENTOR, } } if (collaborationSession.collaboratorsIds.length === 1) { return { id: schedule.id, status: ScheduleDateStatus.MISSING_CUSTOMER, } } if (collaborationSession.collaboratorsIds.length === 2) { return { id: schedule.id, status: ScheduleDateStatus.COMPLETED, } } return null }) .filter((update) => update !== null) for (const update of updates) { await this.prisma.scheduleDate.update({ where: { id: update.id }, data: { status: update.status }, }) } } // cron every 1 minute to check payment status where created_at is more than 15 minutes @Cron(CronExpression.EVERY_MINUTE) async checkPaymentStatus() { Logger.log('Checking payment status', 'checkPaymentStatus') const payments = await this.prisma.payment.findMany({ where: { status: PaymentStatus.PENDING, createdAt: { lt: DateTimeUtils.now().minus({ minutes: 15 }).toJSDate(), }, }, }) Logger.log(`Found ${payments.length} payments to update`, 'checkPaymentStatus') for (const payment of payments) { await this.prisma.payment.update({ where: { id: payment.id }, data: { status: PaymentStatus.CANCELLED }, }) await this.prisma.order.update({ where: { id: payment.orderId }, data: { status: OrderStatus.FAILED }, }) } } // handle refund ticket by order, if order status is refunded, disable schedule and remove schedule date in future @Cron(CronExpression.EVERY_MINUTE) async taskRefundTicket() { Logger.log('Handling refund ticket', 'handleRefundTicket') const now = DateTimeUtils.now().toJSDate() // get all orders where status is REFUNDED and has schedule.dates in future const orders = await this.prisma.order.findMany({ where: { status: OrderStatus.REFUNDED, schedule: { dates: { some: { end: { gt: now, }, }, }, }, }, include: { schedule: { include: { dates: true, }, }, }, }) Logger.log(`Found ${orders.length} orders to handle`, 'handleRefundTicket') for (const order of orders) { await this.prisma.schedule.update({ where: { id: order.scheduleId }, data: { status: ScheduleStatus.REFUNDED }, }) } // remove schedule date in future for (const order of orders) { await this.prisma.scheduleDate.deleteMany({ where: { id: { in: order.schedule.dates.map((d) => d.id) }, start: { gt: now }, }, }) } } // cron every 1 minute to check if there is any schedule date start in less than 30 minutes @Cron(CronExpression.EVERY_MINUTE) async taskCheckScheduleDateStart() { Logger.log('Checking schedule date start', 'taskCheckScheduleDateStart') const schedules = await this.prisma.schedule.findMany({ where: { AND: [ { scheduleStart: { lt: DateTimeUtils.now().plus({ minutes: 30 }).toJSDate(), }, }, { status: ScheduleStatus.PUBLISHED, }, ], }, }) Logger.log(`Found ${schedules.length} schedules to check`, 'taskCheckScheduleDateStart') for (const schedule of schedules) { await this.prisma.scheduleDate.updateMany({ where: { scheduleId: schedule.id }, data: { status: ScheduleDateStatus.EXPIRED }, }) // update schedule status to expired await this.prisma.schedule.update({ where: { id: schedule.id }, data: { status: ScheduleStatus.EXPIRED }, }) // send notification to mentor const managedService = await this.prisma.managedService.findUnique({ where: { id: schedule.managedServiceId }, }) if (managedService) { await this.notificationService.sendNotification( managedService.mentorId, 'Lịch hướng dẫn của bạn đã hết hạn', `Lịch hướng dẫn với ngày bắt đầu: ${DateTimeUtils.format( DateTimeUtils.fromDate(schedule.scheduleStart), 'D', )}, slot: ${schedule.slots.map((s) => s).join(', ')} của bạn đã hết hạn do không có học viên đăng ký, vui lòng tạo lịch hướng dẫn mới`, ) } } } // cron every day to disable service without any schedule in the past 30 days @Cron(CronExpression.EVERY_DAY_AT_1AM) async taskDisableServiceWithoutSchedule() { Logger.log('Disabling service without any schedule', 'taskDisableServiceWithoutSchedule') const services = await this.prisma.managedService.findMany({ where: { NOT: { schedule: { some: { scheduleStart: { gte: DateTimeUtils.now().minus({ days: 30 }).toJSDate() }, }, }, }, }, }) for (const service of services) { await this.prisma.managedService.update({ where: { id: service.id }, data: { service: { update: { status: ServiceStatus.INACTIVE, }, }, }, }) Logger.log(`Service ${service.id} has been disabled`, 'taskDisableServiceWithoutSchedule') } } }