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
+53 -1
View File
@@ -5,6 +5,7 @@ import {
} from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { Prisma } from '@prisma/client';
import ExcelJS from 'exceljs';
import type {
FinancialAccountDto,
FinancialCategoryDto,
@@ -365,7 +366,7 @@ export class TreasuryService {
}
: {}),
},
include: { category: true, account: true, movements: true },
include: { category: true, account: true, movements: true, attachments: { select: { id: true, fileName: true } } },
orderBy: { bookedAt: 'desc' },
take: 200,
});
@@ -555,6 +556,55 @@ export class TreasuryService {
return this.toDocumentDto(fresh);
}
async listAudit(actor: Actor) {
const rows = await this.prisma.financialAuditLog.findMany({
where: { organizationId: actor.organizationId },
orderBy: { createdAt: 'desc' },
take: 200,
});
return rows.map((row) => ({
id: row.id,
entityType: row.entityType,
entityId: row.entityId,
action: row.action,
before: row.before,
after: row.after,
createdAt: row.createdAt.toISOString(),
}));
}
async openAttachment(actor: Actor, id: string) {
const row = await this.prisma.financialAttachment.findFirst({
where: { id, organizationId: actor.organizationId },
});
if (!row) throw new NotFoundException('پیوست پیدا نشد.');
return {
stream: this.files.openReadStream(row.storageKey),
mimeType: row.mimeType,
fileName: row.fileName,
};
}
async documentsExcel(actor: Actor, kind: 'income' | 'expense') {
const rows = await this.listDocuments(actor, kind);
const book = new ExcelJS.Workbook();
const sheet = book.addWorksheet(kind === 'income' ? 'درآمدها' : 'هزینه‌ها');
sheet.addRow(['عنوان', 'مبلغ', 'تسویه', 'مانده', 'وضعیت', 'دسته', 'طرف حساب', 'تاریخ']);
for (const row of rows) {
sheet.addRow([
row.title,
row.amount,
row.settledAmount,
row.remaining,
row.status,
row.categoryName ?? '',
row.partyName ?? '',
row.bookedAt.slice(0, 10),
]);
}
return Buffer.from(await book.xlsx.writeBuffer());
}
async addAttachment(actor: Actor, documentId: string, file: { originalname: string; mimetype: string; buffer: Buffer }) {
const doc = await this.prisma.financialDocument.findFirst({
where: { id: documentId, organizationId: actor.organizationId },
@@ -774,6 +824,7 @@ export class TreasuryService {
category?: { name: string } | null;
account?: { name: string } | null;
movements: Array<{ amount: Prisma.Decimal; voidedAt: Date | null }>;
attachments?: Array<{ id: string; fileName: string }>;
}): FinancialDocumentDto {
const settled = this.settledOf(row.movements);
const rawRemaining = row.amount.minus(settled);
@@ -798,6 +849,7 @@ export class TreasuryService {
reference: row.reference,
notes: row.notes,
createdAt: row.createdAt.toISOString(),
attachments: row.attachments ?? [],
};
}
}