/** * CreditService — Subscriber credit balance application. * * ARCHITECTURE: * Applies subscriber credit balances to invoices. * Called by BillingService after invoice creation (auto-apply on new invoice) * and by PaymentService when overpayments create credits. * * ACCOUNTING TREATMENT: * Credit application journal entry: * DR Accounts Receivable (1100) — reduces the AR balance for this invoice * CR Subscriber Credits (1150) — reduces the credit contra-asset * * Wait — the correct entry for applying credit to invoice is: * DR Subscriber Credits (1150) — debit contra-asset to reduce it (net effect: reduce credit balance) * CR Accounts Receivable (1100) — credit AR to reduce what subscriber owes * * The subscriber.creditBalance is an operational convenience field for FIFO allocation. * It is always updated atomically with the journal entry in the same transaction. */ import { Prisma, InvoiceStatus, JournalEntrySource } from "@prisma/client"; import { JournalEntryService } from "@/lib/accounting/journal-entry-service"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type TenantPrismaClient = any; export interface ApplyCreditResult { appliedAmount: Prisma.Decimal; remainingCredit: Prisma.Decimal; newInvoiceStatus: InvoiceStatus; journalEntryId: string | null; } /** * Apply subscriber credit balance to an invoice. * * - Deducts from subscriber.creditBalance * - Adds to invoice.amountPaid * - Creates journal entry: DR Subscriber Credits (1150), CR AR (1100) * - Updates invoice status to PARTIAL or PAID as appropriate * * All operations are atomic within a single transaction. * * @returns null if the subscriber has no credit balance */ export async function applyCredit( tenantPrisma: TenantPrismaClient, tenantId: string, subscriberId: string, invoiceId: string, createdById: string ): Promise { // Fetch subscriber and invoice together const [subscriber, invoice] = await Promise.all([ tenantPrisma.subscriber.findFirst({ where: { id: subscriberId }, select: { id: true, creditBalance: true }, }), tenantPrisma.invoice.findFirst({ where: { id: invoiceId }, select: { id: true, totalAmount: true, amountPaid: true, status: true, invoiceNumber: true }, }), ]); if (!subscriber) { throw new Error(`Subscriber not found: ${subscriberId}`); } if (!invoice) { throw new Error(`Invoice not found: ${invoiceId}`); } const creditBalance = new Prisma.Decimal(subscriber.creditBalance); if (creditBalance.lessThanOrEqualTo(0)) { return null; // No credit to apply } if (invoice.status === InvoiceStatus.PAID || invoice.status === InvoiceStatus.VOID) { return null; // Invoice already settled — skip } const totalAmount = new Prisma.Decimal(invoice.totalAmount); const alreadyPaid = new Prisma.Decimal(invoice.amountPaid); const remaining = totalAmount.minus(alreadyPaid); if (remaining.lessThanOrEqualTo(0)) { return null; // Already fully paid } // Apply the lesser of credit balance vs remaining balance const appliedAmount = creditBalance.lessThan(remaining) ? creditBalance : remaining; const newAmountPaid = alreadyPaid.plus(appliedAmount); const newCreditBalance = creditBalance.minus(appliedAmount); const isFullyPaid = newAmountPaid.greaterThanOrEqualTo(totalAmount); const newStatus = isFullyPaid ? InvoiceStatus.PAID : InvoiceStatus.PARTIAL; // Find AR and Subscriber Credits accounts const [arAccount, creditsAccount] = await Promise.all([ tenantPrisma.account.findFirst({ where: { code: "1100" }, select: { id: true } }), tenantPrisma.account.findFirst({ where: { code: "1150" }, select: { id: true } }), ]); if (!arAccount || !creditsAccount) { throw new Error("Required accounts (1100 AR, 1150 Subscriber Credits) not found for this tenant"); } // Create the journal entry: // DR Subscriber Credits (1150) — reduces contra-asset (credit balance goes down) // CR Accounts Receivable (1100) — reduces what subscriber owes const journalEntry = await JournalEntryService.createEntry({ tenantPrisma, tenantId, date: new Date(), description: `Credit applied to invoice ${invoice.invoiceNumber}`, source: JournalEntrySource.SYSTEM, referenceType: "Invoice", referenceId: invoiceId, createdById, lines: [ { accountId: creditsAccount.id, debit: appliedAmount.toNumber(), credit: 0, description: `Credit applied: ${appliedAmount.toFixed(2)}`, }, { accountId: arAccount.id, debit: 0, credit: appliedAmount.toNumber(), description: `AR reduced by credit: ${appliedAmount.toFixed(2)}`, }, ], }); // Update subscriber credit balance and invoice in a transaction await tenantPrisma.$transaction(async (tx: TenantPrismaClient) => { await tx.subscriber.update({ where: { id: subscriberId, tenantId }, data: { creditBalance: newCreditBalance }, }); await tx.invoice.update({ where: { id: invoiceId, tenantId }, data: { amountPaid: newAmountPaid, status: newStatus, paidAt: isFullyPaid ? new Date() : null, }, }); }); return { appliedAmount, remainingCredit: newCreditBalance, newInvoiceStatus: newStatus, journalEntryId: journalEntry.id, }; }