Bill all platform SMS (including OTP), allow negative balance with manual/auto cutoffs, and support own-panel bypass without wallet debit.
161 lines
4.5 KiB
TypeScript
161 lines
4.5 KiB
TypeScript
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<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: {
|
|
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<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.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',
|
|
});
|
|
}
|
|
}
|
|
}
|