import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateAccountDto } from './dto/create-coa.dto'; // Default Chart of Accounts for new tenants const DEFAULT_COA = [ { code: '1000', name: 'Assets', type: 'asset', isSystem: true }, { code: '1010', name: 'Cash on Hand', type: 'asset', isSystem: true }, { code: '1020', name: 'GCash Business', type: 'asset', isSystem: true }, { code: '1030', name: 'Maya Business', type: 'asset', isSystem: true }, { code: '1040', name: 'Bank Account', type: 'asset', isSystem: true }, { code: '1100', name: 'Accounts Receivable', type: 'asset', isSystem: true }, { code: '1200', name: 'Equipment', type: 'asset', isSystem: true }, { code: '2000', name: 'Liabilities', type: 'liability', isSystem: true }, { code: '2010', name: 'Accounts Payable', type: 'liability', isSystem: true }, { code: '3000', name: 'Equity', type: 'equity', isSystem: true }, { code: '3010', name: 'Owner\'s Equity', type: 'equity', isSystem: true }, { code: '3020', name: 'Retained Earnings', type: 'equity', isSystem: true }, { code: '4000', name: 'Revenue', type: 'revenue', isSystem: true }, { code: '4010', name: 'Internet Service Revenue', type: 'revenue', isSystem: true }, { code: '4020', name: 'Installation Fees', type: 'revenue', isSystem: true }, { code: '5000', name: 'Expenses', type: 'expense', isSystem: true }, { code: '5010', name: 'Utilities Expense', type: 'expense', isSystem: true }, { code: '5020', name: 'Salaries Expense', type: 'expense', isSystem: true }, { code: '5030', name: 'Maintenance Expense', type: 'expense', isSystem: true }, { code: '5040', name: 'Transport Expense', type: 'expense', isSystem: true }, { code: '5050', name: 'Supplies Expense', type: 'expense', isSystem: true }, { code: '5060', name: 'Equipment Expense', type: 'expense', isSystem: true }, ]; @Injectable() export class AccountingService { constructor(private readonly prisma: PrismaService) {} async getChartOfAccounts(tenantId: string) { const accounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId }, orderBy: { code: 'asc' }, }); // Auto-seed if empty if (accounts.length === 0) { await this.seedDefaultAccounts(tenantId); return this.prisma.chartOfAccount.findMany({ where: { tenantId }, orderBy: { code: 'asc' } }); } return accounts; } async seedDefaultAccounts(tenantId: string) { for (const acc of DEFAULT_COA) { await this.prisma.chartOfAccount.upsert({ where: { tenantId_code: { tenantId, code: acc.code } }, create: { tenantId, ...acc }, update: {}, }); } } async createAccount(tenantId: string, dto: CreateAccountDto) { const existing = await this.prisma.chartOfAccount.findFirst({ where: { tenantId, code: dto.code }, }); if (existing) throw new ConflictException('Account code already exists'); return this.prisma.chartOfAccount.create({ data: { tenantId, code: dto.code, name: dto.name, type: dto.type, parentId: dto.parentId }, }); } async deleteAccount(tenantId: string, id: string) { const account = await this.prisma.chartOfAccount.findFirst({ where: { id, tenantId } }); if (!account) throw new NotFoundException('Account not found'); if (account.isSystem) throw new BadRequestException('Cannot delete system account'); const hasEntries = await this.prisma.journalLine.count({ where: { accountId: id } }); if (hasEntries > 0) throw new BadRequestException('Cannot delete account with journal entries'); await this.prisma.chartOfAccount.delete({ where: { id } }); return { deleted: true }; } async getGeneralLedger(tenantId: string, accountId?: string) { const where: any = {}; if (accountId) { where.accountId = accountId; } else { where.journalEntry = { tenantId }; } return this.prisma.journalLine.findMany({ where, include: { account: { select: { code: true, name: true, type: true } }, journalEntry: { select: { entryDate: true, description: true, reference: true } }, }, orderBy: { journalEntry: { entryDate: 'desc' } }, take: 100, }); } async getTrialBalance(tenantId: string) { const accounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId }, include: { journalLines: { select: { debit: true, credit: true } }, }, orderBy: { code: 'asc' }, }); return accounts.map((a) => { const totalDebit = a.journalLines.reduce((s, l) => s + Number(l.debit), 0); const totalCredit = a.journalLines.reduce((s, l) => s + Number(l.credit), 0); return { id: a.id, code: a.code, name: a.name, type: a.type, debit: totalDebit, credit: totalCredit, balance: totalDebit - totalCredit, }; }).filter((a) => a.debit > 0 || a.credit > 0); } async getAccountingOverview(tenantId: string) { const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); // All accounts with their full journal lines const accounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId }, include: { journalLines: { select: { debit: true, credit: true, journalEntry: { select: { entryDate: true } } }, }, }, orderBy: { code: 'asc' }, }); // Compute balances by type (from all journal entries) const byType: Record = {}; const cashAccounts: { code: string; name: string; balance: number }[] = []; for (const acc of accounts) { const totalDebit = acc.journalLines.reduce((s, l) => s + Number(l.debit), 0); const totalCredit = acc.journalLines.reduce((s, l) => s + Number(l.credit), 0); const balance = totalDebit - totalCredit; if (!byType[acc.type]) byType[acc.type] = { debit: 0, credit: 0, balance: 0 }; byType[acc.type].debit += totalDebit; byType[acc.type].credit += totalCredit; byType[acc.type].balance += balance; // Track individual cash accounts (codes 1010-1040) if (['1010', '1020', '1030', '1040'].includes(acc.code)) { cashAccounts.push({ code: acc.code, name: acc.name, balance }); } } // Monthly income (revenue credits this month) const monthlyRevenue = accounts .filter((a) => a.type === 'revenue') .reduce((sum, acc) => { const monthCredits = acc.journalLines .filter((l) => new Date(l.journalEntry.entryDate) >= monthStart) .reduce((s, l) => s + Number(l.credit), 0); return sum + monthCredits; }, 0); // Monthly expenses (expense debits this month) const monthlyExpenses = accounts .filter((a) => a.type === 'expense') .reduce((sum, acc) => { const monthDebits = acc.journalLines .filter((l) => new Date(l.journalEntry.entryDate) >= monthStart) .reduce((s, l) => s + Number(l.debit), 0); return sum + monthDebits; }, 0); // Expense breakdown by category this month const expenseBreakdown = accounts .filter((a) => a.type === 'expense') .map((acc) => ({ code: acc.code, name: acc.name, amount: acc.journalLines .filter((l) => new Date(l.journalEntry.entryDate) >= monthStart) .reduce((s, l) => s + Number(l.debit), 0), })) .filter((e) => e.amount > 0) .sort((a, b) => b.amount - a.amount); return { totalAssets: byType['asset']?.balance || 0, totalLiabilities: byType['liability']?.balance || 0, totalEquity: byType['equity']?.balance || 0, totalRevenue: byType['revenue']?.credit || 0, totalExpenses: byType['expense']?.debit || 0, accountsReceivable: accounts.find((a) => a.code === '1100')?.journalLines.reduce((s, l) => s + Number(l.debit) - Number(l.credit), 0) || 0, cashAccounts, cashOnHand: cashAccounts.reduce((s, a) => s + a.balance, 0), monthlyRevenue, monthlyExpenses, netIncome: monthlyRevenue - monthlyExpenses, expenseBreakdown, }; } }