138 lines
5.4 KiB
TypeScript
138 lines
5.4 KiB
TypeScript
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' },
|
|
});
|
|
}
|
|
}
|