Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { CustomerInvoice, CustomerFinancialProfile, CustomerReceipt } from '../schemas/accounts-receivable.schema'; import { JournalEntry, JournalLine } from '../schemas/journal-entry.schema'; import { PostingEngineService } from './posting-engine.service'; import { LedgerAccount } from '../schemas/ledger-account.schema'; @Injectable() export class AccountsReceivableService { constructor( @InjectModel(CustomerInvoice.name) private readonly invoiceModel: Model<CustomerInvoice>, @InjectModel(CustomerFinancialProfile.name) private readonly profileModel: Model<CustomerFinancialProfile>, @InjectModel(CustomerReceipt.name) private readonly receiptModel: Model<CustomerReceipt>, @InjectModel(JournalEntry.name) private readonly journalModel: Model<JournalEntry>, @InjectModel(JournalLine.name) private readonly lineModel: Model<JournalLine>, @InjectModel(LedgerAccount.name) private readonly accountModel: Model<LedgerAccount>, private readonly postingEngine: PostingEngineService, ) {} async createProfile(tenantId: string, data: any): Promise<CustomerFinancialProfile> { const doc = new this.profileModel({ ...data, tenantId: new Types.ObjectId(tenantId) }); return doc.save(); } async getProfile(tenantId: string, customerId: string): Promise<CustomerFinancialProfile> { const prof = await this.profileModel.findOne({ customerId, tenantId }).exec(); if (!prof) throw new NotFoundException('Customer financial profile not found'); return prof; } async registerInvoice(tenantId: string, data: any, userId: string): Promise<CustomerInvoice> { const profile = await this.getProfile(tenantId, data.customerId); const totalMinor = data.totalMinor; const subtotalMinor = data.subtotalMinor || totalMinor; const taxMinor = data.taxMinor || 0; const invoiceDate = data.invoiceDate || new Date(); const dueDate = new Date(invoiceDate); dueDate.setDate(dueDate.getDate() + (profile.defaultPaymentTermsDays || 30)); const invoice = new this.invoiceModel({ tenantId: new Types.ObjectId(tenantId), legalEntityId: new Types.ObjectId(data.legalEntityId), customerId: data.customerId, invoiceNumber: data.invoiceNumber, sourceDocumentType: data.sourceDocumentType, sourceDocumentId: data.sourceDocumentId, invoiceDate, dueDate, subtotalMinor, taxMinor, totalMinor, outstandingAmountMinor: totalMinor, status: 'approved' }); await invoice.save(); profile.outstandingBalanceMinor += totalMinor; await profile.save(); // Trigger Posting Engine: Debit AR Control, Credit Revenue const arControlAccount = profile.accountsReceivableControlAccountId; if (!arControlAccount) { throw new BadRequestException('No AR control account configured for customer profile'); } const revenueAccount = await this.accountModel.findOne({ tenantId, accountSubtype: 'Revenue' }).exec(); if (!revenueAccount) { throw new BadRequestException('Revenue ledger account not configured'); } const jv = new this.journalModel({ tenantId: new Types.ObjectId(tenantId), legalEntityId: invoice.legalEntityId, accountingBookId: data.accountingBookId || new Types.ObjectId(), journalNumber: `AR-${invoice.invoiceNumber}`, postingDate: new Date(), status: 'draft', journalSource: 'Accounts Receivable', currency: 'INR', exchangeRate: 1.0 }); await jv.save(); // AR Control Debit line await this.lineModel.create({ journalEntryId: jv._id, ledgerAccountId: arControlAccount, debitAmountMinor: totalMinor, creditAmountMinor: 0, description: `AR Control debit for invoice ${invoice.invoiceNumber}` }); // Revenue Credit line await this.lineModel.create({ journalEntryId: jv._id, ledgerAccountId: revenueAccount._id, debitAmountMinor: 0, creditAmountMinor: totalMinor, description: `Revenue credit for invoice ${invoice.invoiceNumber}` }); await this.postingEngine.postJournalEntry(tenantId, jv._id.toString(), userId); invoice.status = 'posted'; await invoice.save(); return invoice; } async processReceipt(tenantId: string, data: any, userId: string): Promise<CustomerReceipt> { const receipt = new this.receiptModel({ tenantId: new Types.ObjectId(tenantId), customerId: data.customerId, bankAccountId: new Types.ObjectId(data.bankAccountId), receiptDate: new Date(), amountMinor: data.amountMinor, status: 'posted', receiptReference: data.receiptReference }); await receipt.save(); // Deduct from customer profile balance const profile = await this.getProfile(tenantId, data.customerId); profile.outstandingBalanceMinor = Math.max(0, profile.outstandingBalanceMinor - data.amountMinor); await profile.save(); // Allocate receipt to outstanding invoices let remainingAmount = data.amountMinor; const invoices = await this.invoiceModel .find({ tenantId, customerId: data.customerId, outstandingAmountMinor: { $gt: 0 } }) .sort({ invoiceDate: 1 }) .exec(); for (const inv of invoices) { if (remainingAmount <= 0) break; const allocated = Math.min(inv.outstandingAmountMinor, remainingAmount); inv.outstandingAmountMinor -= allocated; remainingAmount -= allocated; if (inv.outstandingAmountMinor === 0) { inv.status = 'paid'; } await inv.save(); } return receipt; } async getAgeingReport(tenantId: string): Promise<any> { const invoices = await this.invoiceModel.find({ tenantId, outstandingAmountMinor: { $gt: 0 } }).exec(); const now = new Date(); const report = { current: 0, days30: 0, days60: 0, days90: 0, over90: 0 }; for (const inv of invoices) { const diffTime = Math.abs(now.getTime() - inv.dueDate.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (inv.dueDate > now) { report.current += inv.outstandingAmountMinor; } else if (diffDays <= 30) { report.days30 += inv.outstandingAmountMinor; } else if (diffDays <= 60) { report.days60 += inv.outstandingAmountMinor; } else if (diffDays <= 90) { report.days90 += inv.outstandingAmountMinor; } else { report.over90 += inv.outstandingAmountMinor; } } return report; } } |