initial: standalone repo from monorepo split
This commit is contained in:
137
src/account/account.service.ts
Normal file
137
src/account/account.service.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user