Let cheques be received or paid without an invoice, and remind staff three days before due date.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SmsService } from '../common/sms.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: SmsService,
|
||||
) {}
|
||||
|
||||
@Cron('0 8 * * *', { timeZone: 'Asia/Tehran' })
|
||||
async handleDaily(): Promise<void> {
|
||||
await this.notifyDueSoon();
|
||||
}
|
||||
|
||||
async notifyDueSoon(now: Date = new Date()): Promise<number> {
|
||||
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: {
|
||||
organizationId: row.organizationId,
|
||||
OR: [
|
||||
{ role: { in: ['owner', 'admin'] } },
|
||||
...(row.ownerId ? [{ id: row.ownerId }] : []),
|
||||
],
|
||||
},
|
||||
select: { id: true, mobile: true },
|
||||
});
|
||||
const seen = new Set<string>();
|
||||
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.sendSmsDetailed(user.mobile, staffText);
|
||||
}
|
||||
}
|
||||
|
||||
if (incoming && row.person?.mobile) {
|
||||
await this.sms.sendSmsDetailed(
|
||||
row.person.mobile,
|
||||
personIncomingSms({
|
||||
number: row.number,
|
||||
amount,
|
||||
currency: row.currency,
|
||||
dueDate: row.dueDate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user