feat: gate treasury as an org module and group money inputs

Link optional invoice and cheque settlements to cash accounts, and let superadmin enable the finance module per organization.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 19:33:09 +03:30
co-authored by Cursor
parent ab538b8304
commit 8981a552fc
25 changed files with 509 additions and 33 deletions
+34
View File
@@ -13,6 +13,7 @@ import type {
TreasuryDashboardDto,
} from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { LocalFileStorage } from '../storage/local-file.storage';
import type { Actor } from '../common/record-scope';
import { TreasuryReceivablesService } from './treasury-receivables.service';
@@ -58,6 +59,7 @@ export class TreasuryService {
constructor(
private readonly prisma: PrismaService,
private readonly receivables: TreasuryReceivablesService,
private readonly files: LocalFileStorage,
) {}
async ensureCategories(organizationId: string) {
@@ -507,6 +509,16 @@ export class TreasuryService {
data: { status, accountId: account.id },
}),
]);
await this.prisma.financialAuditLog.create({
data: {
organizationId: actor.organizationId,
entityType: 'document',
entityId: doc.id,
action: 'settle',
after: { amount: amount.toFixed(2), accountId: account.id, status },
actorUserId: actor.userId,
},
}).catch(() => undefined);
if (status === 'settled') {
await this.prisma.financialRecurringOccurrence.updateMany({
where: { documentId: doc.id, status: 'pending' },
@@ -543,6 +555,28 @@ export class TreasuryService {
return this.toDocumentDto(fresh);
}
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 },
select: { id: true },
});
if (!doc) throw new NotFoundException('سند پیدا نشد.');
const safeName = file.originalname.replace(/[^\w.\-\u0600-\u06FF]+/g, '_').slice(0, 80);
const storageKey = `treasury/${actor.organizationId}/${doc.id}/${randomUUID()}-${safeName || 'file'}`;
await this.files.save(storageKey, file.buffer);
return this.prisma.financialAttachment.create({
data: {
organizationId: actor.organizationId,
documentId: doc.id,
fileName: file.originalname,
mimeType: file.mimetype || 'application/octet-stream',
storageKey,
createdById: actor.userId,
},
select: { id: true, fileName: true, createdAt: true },
});
}
async dashboard(
actor: Actor,
range?: { from?: string; to?: string },