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:
@@ -12,12 +12,18 @@ import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { type ScopedActor, ownerWhere, scopedOwnerIds } from '../common/record-scope';
|
||||
import { buildSearchWhere, ci } from '../common/search';
|
||||
import { TreasuryBridgeService } from '../treasury/treasury-bridge.service';
|
||||
import { BillingService } from '../billing/billing.service';
|
||||
|
||||
const CHEQUE_STATUSES = ['pending', 'cleared', 'bounced'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class ChequesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly treasury: TreasuryBridgeService,
|
||||
private readonly billing: BillingService,
|
||||
) {}
|
||||
|
||||
async list(
|
||||
organizationId: string,
|
||||
@@ -92,9 +98,13 @@ export class ChequesService {
|
||||
? dto.status
|
||||
: 'pending',
|
||||
notes: dto.notes?.trim() ? dto.notes.trim() : null,
|
||||
financialAccountId: dto.financialAccountId || null,
|
||||
},
|
||||
include: this.include,
|
||||
});
|
||||
if (row.status === 'cleared' && row.financialAccountId && await this.treasuryEnabled(organizationId)) {
|
||||
await this.postCleared(row, actor.userId);
|
||||
}
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
@@ -106,7 +116,6 @@ export class ChequesService {
|
||||
): Promise<ChequeDto> {
|
||||
const existing = await this.prisma.cheque.findFirst({
|
||||
where: { id, organizationId, ...this.accessWhere(actor) },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('چک پیدا نشد');
|
||||
|
||||
@@ -123,15 +132,61 @@ export class ChequesService {
|
||||
if ('notes' in dto) {
|
||||
data.notes = dto.notes?.trim() ? dto.notes.trim() : null;
|
||||
}
|
||||
if (dto.financialAccountId) data.financialAccountId = dto.financialAccountId;
|
||||
const nextStatus = dto.status ?? existing.status;
|
||||
const nextAccount = dto.financialAccountId || existing.financialAccountId;
|
||||
const treasuryOn = await this.treasuryEnabled(organizationId);
|
||||
if (treasuryOn && existing.status !== 'cleared' && nextStatus === 'cleared' && !nextAccount) {
|
||||
throw new BadRequestException('برای وصول چک، حساب صندوق را انتخاب کنید.');
|
||||
}
|
||||
|
||||
const row = await this.prisma.cheque.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: this.include,
|
||||
});
|
||||
if (
|
||||
existing.status !== 'cleared' &&
|
||||
row.status === 'cleared' &&
|
||||
row.financialAccountId &&
|
||||
await this.treasuryEnabled(organizationId)
|
||||
) {
|
||||
await this.postCleared(row, actor.userId);
|
||||
}
|
||||
if (existing.status === 'cleared' && row.status !== 'cleared') {
|
||||
await this.treasury.voidCheque(row.id);
|
||||
if (row.paymentId) await this.treasury.voidPayment(row.paymentId);
|
||||
}
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
private async treasuryEnabled(organizationId: string) {
|
||||
try {
|
||||
await this.billing.requireModule(organizationId, 'treasury');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async postCleared(
|
||||
row: { id: string; organizationId: string; financialAccountId: string | null; amount: Prisma.Decimal; direction: string; number: string },
|
||||
actorUserId: string,
|
||||
) {
|
||||
if (!row.financialAccountId) return;
|
||||
await this.treasury.postCash({
|
||||
organizationId: row.organizationId,
|
||||
accountId: row.financialAccountId,
|
||||
amount: row.amount,
|
||||
direction: row.direction === 'out' ? 'out' : 'in',
|
||||
kind: row.direction === 'out' ? 'payment' : 'receipt',
|
||||
occurredAt: new Date(),
|
||||
memo: `وصول چک ${row.number}`,
|
||||
actorUserId,
|
||||
chequeId: row.id,
|
||||
});
|
||||
}
|
||||
|
||||
private accessWhere(
|
||||
actor: ScopedActor,
|
||||
): Prisma.ChequeWhereInput {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { TreasuryModule } from '../treasury/treasury.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { SmsService } from '../common/sms.service';
|
||||
import { ChequesService } from './cheques.service';
|
||||
@@ -8,7 +9,7 @@ import { CommissionsService } from './commissions.service';
|
||||
import { FinanceController } from './finance.controller';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, NotificationsModule],
|
||||
imports: [AuthModule, NotificationsModule, TreasuryModule],
|
||||
controllers: [FinanceController],
|
||||
providers: [ChequesService, ChequeRemindersService, CommissionsService, SmsService],
|
||||
exports: [ChequesService, CommissionsService],
|
||||
|
||||
Reference in New Issue
Block a user