feat: add treasury audit, attachment download, and installment SMS

Let cheque clearance pick a cash account and export income, expense, and installment lists to Excel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 19:59:24 +03:30
co-authored by Cursor
parent 8981a552fc
commit 96befd3867
11 changed files with 285 additions and 21 deletions
@@ -13,6 +13,8 @@ import type {
} from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { SmsService } from '../common/sms.service';
import ExcelJS from 'exceljs';
import type { Actor } from '../common/record-scope';
function dec(value: Prisma.Decimal | number | string | null | undefined) {
@@ -45,6 +47,7 @@ export class TreasuryReceivablesService {
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
private readonly sms: SmsService,
) {}
async list(actor: Actor): Promise<FinancialReceivableDto[]> {
@@ -211,6 +214,27 @@ export class TreasuryReceivablesService {
return this.get(actor, installment.receivableId);
}
async excel(actor: Actor) {
const rows = await this.list(actor);
const book = new ExcelJS.Workbook();
const sheet = book.addWorksheet('اقساط');
sheet.addRow(['قرارداد', 'طرف حساب', 'قسط', 'سررسید', 'مبلغ', 'مانده', 'وضعیت']);
for (const row of rows) {
for (const item of row.installments) {
sheet.addRow([
row.title,
row.partyName ?? '',
item.position,
item.dueDate.slice(0, 10),
item.amount,
item.remaining,
item.status,
]);
}
}
return Buffer.from(await book.xlsx.writeBuffer());
}
async upcoming(actor: Actor) {
const rows = await this.prisma.financialInstallment.findMany({
where: {
@@ -264,18 +288,27 @@ export class TreasuryReceivablesService {
},
select: { userId: true },
});
const title = dto.status === 'overdue' ? 'قسط معوق' : 'قسط نزدیک سررسید';
const body = `${row.receivable.title} · قسط ${row.position} · مانده ${dto.remaining}`;
for (const admin of admins) {
await this.notifications.create({
organizationId: row.receivable.organizationId,
userId: admin.userId,
type: 'treasury_installment',
title: dto.status === 'overdue' ? 'قسط معوق' : 'قسط نزدیک سررسید',
body: `${row.receivable.title} · قسط ${row.position}`,
title,
body,
link: '/finance/receivables',
entityType: 'financial_installment',
entityId: row.id,
dedupe: true,
});
const user = await this.prisma.user.findUnique({
where: { id: admin.userId },
select: { mobile: true },
});
if (user?.mobile && this.sms.isConfigured()) {
await this.sms.sendSmsDetailed(user.mobile, `${title}: ${body}`).catch(() => undefined);
}
}
await this.prisma.financialInstallment.update({
where: { id: row.id },