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
+2 -1
View File
@@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
import { AutomationsModule } from '../automations/automations.module';
import { MessageAutomationsModule } from '../message-automations/message-automations.module';
import { FinanceModule } from '../finance/finance.module';
import { TreasuryModule } from '../treasury/treasury.module';
import { InventoryModule } from '../inventory/inventory.module';
import { InvoicesController } from './invoices.controller';
import { InvoicesService } from './invoices.service';
@Module({
imports: [AutomationsModule, MessageAutomationsModule, InventoryModule, FinanceModule],
imports: [AutomationsModule, MessageAutomationsModule, InventoryModule, FinanceModule, TreasuryModule],
controllers: [InvoicesController],
providers: [InvoicesService],
exports: [InvoicesService],
+29
View File
@@ -22,6 +22,7 @@ import { MessageAutomationsService } from '../message-automations/message-automa
import { InventoryService } from '../inventory/inventory.service';
import { CommissionsService } from '../finance/commissions.service';
import { GamificationService } from '../gamification/gamification.service';
import { TreasuryBridgeService } from '../treasury/treasury-bridge.service';
type InvoiceActor = Pick<Actor, 'userId' | 'role'>;
@@ -54,6 +55,7 @@ export class InvoicesService {
private readonly inventory: InventoryService,
private readonly commissions: CommissionsService,
private readonly game: GamificationService,
private readonly treasury: TreasuryBridgeService,
) {}
async list(
@@ -509,9 +511,16 @@ export class InvoicesService {
? dto.cheque.status
: 'pending',
notes: emptyToNull(dto.cheque.notes ?? null),
financialAccountId: dto.financialAccountId || null,
},
});
}
if (dto.financialAccountId) {
await tx.payment.update({
where: { id: payment.id },
data: { financialAccountId: dto.financialAccountId },
});
}
if (installmentId) {
await tx.invoiceInstallment.update({
where: { id: installmentId },
@@ -530,6 +539,25 @@ export class InvoicesService {
return payment.id;
});
const accountId = dto.financialAccountId || null;
const chequePending = method === 'check' && dto.cheque?.status !== 'cleared';
if (accountId && !chequePending) {
const linkedCheque = method === 'check'
? await this.prisma.cheque.findFirst({ where: { paymentId }, select: { id: true } })
: null;
await this.treasury.postCash({
organizationId,
accountId,
amount: dto.amount,
direction: 'in',
kind: 'receipt',
occurredAt: dto.paidAt ? new Date(dto.paidAt) : new Date(),
memo: 'پرداخت فاکتور',
actorUserId: actor.userId,
paymentId,
chequeId: linkedCheque?.id ?? null,
});
}
await this.recomputeStatus(invoiceId);
const row = await this.get(organizationId, invoiceId, actor);
await this.inventory.syncInvoiceStock(organizationId, row.id);
@@ -574,6 +602,7 @@ export class InvoicesService {
});
if (!payment) throw new NotFoundException('پرداخت پیدا نشد');
await this.treasury.voidPayment(paymentId);
await this.prisma.$transaction(async (tx) => {
if (payment.installmentId) {
await tx.invoiceInstallment.update({