import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { NotificationsService } from '../notifications/notifications.service'; import { PrismaService } from '../prisma/prisma.service'; import { SmsGatewayService } from '../sms-billing/sms-gateway.service'; import { CHEQUE_REMINDER_DAYS, isDueInDays, personIncomingSms, staffIncomingSms, staffOutgoingSms, } from './cheque-reminder.util'; @Injectable() export class ChequeRemindersService { private readonly logger = new Logger(ChequeRemindersService.name); constructor( private readonly prisma: PrismaService, private readonly notifications: NotificationsService, private readonly sms: SmsGatewayService, ) {} @Cron('0 8 * * *', { timeZone: 'Asia/Tehran' }) async handleDaily(): Promise { await this.notifyDueSoon(); } async notifyDueSoon(now: Date = new Date()): Promise { const from = new Date(now.getTime() - 24 * 60 * 60 * 1000); const to = new Date(now.getTime() + 6 * 24 * 60 * 60 * 1000); const rows = await this.prisma.cheque.findMany({ where: { status: 'pending', reminderNotifiedAt: null, dueDate: { gte: from, lte: to }, }, include: { person: { select: { firstName: true, lastName: true, mobile: true } }, }, take: 500, }); let sent = 0; for (const row of rows) { if (!isDueInDays(row.dueDate, CHEQUE_REMINDER_DAYS, now)) continue; try { await this.notifyOne(row); sent += 1; } catch (err) { this.logger.warn( `cheque reminder failed ${row.id}: ${err instanceof Error ? err.message : 'unknown'}`, ); } finally { await this.prisma.cheque.update({ where: { id: row.id }, data: { reminderNotifiedAt: new Date() }, }); } } if (sent) this.logger.log(`cheque reminders sent: ${sent}`); return sent; } private async notifyOne(row: { id: string; organizationId: string; ownerId: string | null; direction: string; number: string; amount: { toString(): string } | number; currency: string; dueDate: Date; person: { firstName: string; lastName: string; mobile: string | null } | null; }) { const personName = row.person ? `${row.person.firstName} ${row.person.lastName}`.trim() : ''; const amount = Number(row.amount); const incoming = row.direction !== 'out'; const staffText = incoming ? staffIncomingSms({ number: row.number, personName, amount, currency: row.currency, dueDate: row.dueDate, }) : staffOutgoingSms({ number: row.number, personName, amount, currency: row.currency, dueDate: row.dueDate, }); const staff = await this.prisma.user.findMany({ where: { OR: [ { memberships: { some: { organizationId: row.organizationId, role: { in: ['owner', 'admin'] }, isActive: true, }, }, }, ...(row.ownerId ? [ { id: row.ownerId, memberships: { some: { organizationId: row.organizationId } }, }, ] : []), ], }, select: { id: true, mobile: true }, }); const seen = new Set(); for (const user of staff) { if (seen.has(user.id)) continue; seen.add(user.id); await this.notifications.create({ organizationId: row.organizationId, userId: user.id, type: 'cheque_due', title: 'سررسید چک', body: staffText.replace(/^یادآوری منتکت:\s*/, ''), link: '/cheques', entityType: 'cheque', entityId: row.id, dedupe: true, }); if (user.mobile) { await this.sms.send({ organizationId: row.organizationId, phone: user.mobile, text: staffText, source: 'cheque_reminder', }); } } if (incoming && row.person?.mobile) { await this.sms.send({ organizationId: row.organizationId, phone: row.person.mobile, text: personIncomingSms({ number: row.number, amount, currency: row.currency, dueDate: row.dueDate, }), source: 'cheque_reminder', }); } } }