All files / src/domains/finance/services accounts-payable.service.ts

0% Statements 0/67
0% Branches 0/58
0% Functions 0/5
0% Lines 0/64

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                                                                                                                                                                                                                                                                                                                                       
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { AccountsPayableInvoice, VendorFinancialProfile, VendorPayment } from '../schemas/accounts-payable.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 AccountsPayableService {
  constructor(
    @InjectModel(AccountsPayableInvoice.name)
    private readonly invoiceModel: Model<AccountsPayableInvoice>,
    @InjectModel(VendorFinancialProfile.name)
    private readonly profileModel: Model<VendorFinancialProfile>,
    @InjectModel(VendorPayment.name)
    private readonly paymentModel: Model<VendorPayment>,
    @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<VendorFinancialProfile> {
    const doc = new this.profileModel({
      ...data,
      tenantId: new Types.ObjectId(tenantId)
    });
    return doc.save();
  }
 
  async getProfile(tenantId: string, vendorId: string): Promise<VendorFinancialProfile> {
    const prof = await this.profileModel.findOne({ vendorId, tenantId }).exec();
    if (!prof) throw new NotFoundException('Vendor financial profile not found');
    return prof;
  }
 
  async registerInvoiceFromBill(tenantId: string, billData: any, userId: string): Promise<AccountsPayableInvoice> {
    // Check duplicate
    const duplicate = await this.invoiceModel.findOne({
      tenantId,
      invoiceNumber: billData.billNumber
    }).exec();
    if (duplicate) {
      throw new BadRequestException(`Duplicate invoice detected for bill: ${billData.billNumber}`);
    }
 
    const profile = await this.getProfile(tenantId, billData.vendorId);
    
    const invoiceDate = billData.billDate || new Date();
    const dueDate = new Date(invoiceDate);
    dueDate.setDate(dueDate.getDate() + (profile.defaultPaymentTermsDays || 30));
 
    const totalMinor = billData.totalAmountMinor || Math.round(billData.totalAmount * 100);
    const taxMinor = billData.taxMinor || Math.round((billData.tax || 0) * 100);
    const subtotalMinor = totalMinor - taxMinor;
 
    const invoice = new this.invoiceModel({
      tenantId: new Types.ObjectId(tenantId),
      legalEntityId: new Types.ObjectId(billData.legalEntityId),
      vendorId: billData.vendorId,
      invoiceNumber: billData.billNumber,
      billId: billData.billId || billData._id?.toString(),
      invoiceDate,
      dueDate,
      subtotalMinor,
      taxMinor,
      totalMinor,
      outstandingAmountMinor: totalMinor,
      status: 'approved'
    });
    await invoice.save();
 
    // Update outstanding balance on profile
    profile.outstandingBalanceMinor += totalMinor;
    await profile.save();
 
    // Trigger Posting Engine: AP Control Account credit, Expense/Inventory debit
    const apAccount = profile.accountsPayableControlAccountId;
    if (!apAccount) {
      throw new BadRequestException('No AP control account configured for vendor profile');
    }
 
    // Load matching inventory control account or other expense account
    const expenseAccount = await this.accountModel.findOne({
      tenantId,
      accountSubtype: 'Cost of Goods Sold'
    }).exec();
    if (!expenseAccount) {
      throw new BadRequestException('Cost of Goods Sold ledger account not configured');
    }
 
    const jv = new this.journalModel({
      tenantId: new Types.ObjectId(tenantId),
      legalEntityId: invoice.legalEntityId,
      accountingBookId: billData.accountingBookId || new Types.ObjectId(),
      journalNumber: `AP-${invoice.invoiceNumber}`,
      postingDate: new Date(),
      status: 'draft',
      journalSource: 'Accounts Payable',
      currency: 'INR',
      exchangeRate: 1.0
    });
    await jv.save();
 
    // Expense Debit line
    await this.lineModel.create({
      journalEntryId: jv._id,
      ledgerAccountId: expenseAccount._id,
      debitAmountMinor: totalMinor,
      creditAmountMinor: 0,
      description: `Expense posting for invoice ${invoice.invoiceNumber}`
    });
 
    // AP Payable Control Credit line
    await this.lineModel.create({
      journalEntryId: jv._id,
      ledgerAccountId: apAccount,
      debitAmountMinor: 0,
      creditAmountMinor: totalMinor,
      description: `AP control credit for invoice ${invoice.invoiceNumber}`
    });
 
    await this.postingEngine.postJournalEntry(tenantId, jv._id.toString(), userId);
    invoice.status = 'posted';
    await invoice.save();
 
    return invoice;
  }
 
  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;
  }
}