initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:55 +08:00
commit 4a8f1e9318
157 changed files with 9198 additions and 0 deletions

View 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' },
});
}
}