import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { JournalService } from '../accounting/journal.service'; import { CreateAccountDto } from './dto/create-account.dto'; import { UpdateAccountDto } from './dto/update-account.dto'; import { TransferFundsDto } from './dto/transfer-funds.dto'; @Injectable() export class AccountService { constructor( private readonly prisma: PrismaService, private readonly journal: JournalService, ) {} async findAll(tenantId: string) { return this.prisma.companyAccount.findMany({ where: { tenantId }, orderBy: [{ isSystem: 'desc' }, { name: 'asc' }], }); } async create(tenantId: string, dto: CreateAccountDto) { // Auto-generate a CoA code for this account const existingCoa = await this.prisma.chartOfAccount.findMany({ where: { tenantId, type: 'asset', code: { startsWith: '10' } }, orderBy: { code: 'desc' }, take: 1, }); const lastCode = existingCoa[0]?.code || '1040'; const nextCode = String(parseInt(lastCode) + 10); // Create CoA entry first const coa = await this.prisma.chartOfAccount.create({ data: { tenantId, code: nextCode, name: dto.name, type: 'asset', isSystem: false }, }); // Create company account linked to CoA return this.prisma.companyAccount.create({ data: { tenantId, name: dto.name, type: dto.type, accountNo: dto.accountNo, balance: dto.initialBalance || 0, chartOfAccountId: coa.id, }, }); } async update(tenantId: string, id: string, dto: UpdateAccountDto) { const account = await this.prisma.companyAccount.findFirst({ where: { id, tenantId } }); if (!account) throw new NotFoundException('Account not found'); if (account.isSystem && dto.name && dto.name !== account.name) { throw new ForbiddenException('Cannot rename system account'); } const updated = await this.prisma.companyAccount.update({ where: { id }, data: { ...(dto.name && { name: dto.name }), ...(dto.accountNo !== undefined && { accountNo: dto.accountNo }), ...(dto.isActive !== undefined && { isActive: dto.isActive }), }, }); // Sync CoA name if changed if (dto.name && account.chartOfAccountId) { await this.prisma.chartOfAccount.update({ where: { id: account.chartOfAccountId }, data: { name: dto.name }, }).catch(() => {}); } return updated; } async remove(tenantId: string, id: string) { const account = await this.prisma.companyAccount.findFirst({ where: { id, tenantId } }); if (!account) throw new NotFoundException('Account not found'); if (account.isSystem) throw new ForbiddenException('Cannot delete system account (Cash on Hand)'); if (Number(account.balance) > 0) throw new BadRequestException('Transfer funds out before deleting'); // Delete linked CoA if exists if (account.chartOfAccountId) { await this.prisma.chartOfAccount.delete({ where: { id: account.chartOfAccountId } }).catch(() => {}); } await this.prisma.companyAccount.delete({ where: { id } }); return { deleted: true }; } async transfer(tenantId: string, transferredBy: string, dto: TransferFundsDto) { const from = await this.prisma.companyAccount.findFirst({ where: { id: dto.fromAccountId, tenantId } }); const to = await this.prisma.companyAccount.findFirst({ where: { id: dto.toAccountId, tenantId } }); if (!from || !to) throw new NotFoundException('Account not found'); if (from.id === to.id) throw new BadRequestException('Cannot transfer to same account'); if (Number(from.balance) < dto.amount) throw new BadRequestException('Insufficient balance'); const [transfer] = await this.prisma.$transaction([ this.prisma.fundTransfer.create({ data: { tenantId, fromAccountId: dto.fromAccountId, toAccountId: dto.toAccountId, amount: dto.amount, description: dto.description, transferredBy }, }), this.prisma.companyAccount.update({ where: { id: from.id }, data: { balance: { decrement: dto.amount } } }), this.prisma.companyAccount.update({ where: { id: to.id }, data: { balance: { increment: dto.amount } } }), ]); // Journal entry for fund transfer: DR destination CoA, CR source CoA if (from.chartOfAccountId && to.chartOfAccountId) { const fromCoa = await this.prisma.chartOfAccount.findUnique({ where: { id: from.chartOfAccountId } }); const toCoa = await this.prisma.chartOfAccount.findUnique({ where: { id: to.chartOfAccountId } }); if (fromCoa && toCoa) { this.journal.createEntry( tenantId, `Fund transfer: ${from.name} → ${to.name}${dto.description ? ` — ${dto.description}` : ''}`, [ { accountCode: toCoa.code, debit: dto.amount }, { accountCode: fromCoa.code, credit: dto.amount }, ], { reference: `TRF-${transfer.id.slice(0, 8)}`, sourceType: 'transfer', sourceId: transfer.id }, ).catch(() => {}); } } return transfer; } async getTransfers(tenantId: string) { return this.prisma.fundTransfer.findMany({ where: { tenantId }, include: { fromAccount: { select: { name: true, type: true } }, toAccount: { select: { name: true, type: true } }, }, orderBy: { createdAt: 'desc' }, }); } }