feat(02-04): Invoice model, BillingService, InvoiceService, CreditService
- Add InvoiceStatus enum + Invoice + InvoiceLine models to schema - Add Invoice relation to Subscriber model - Add invoice/invoiceLine to TENANT_SCOPED_MODELS in prisma-tenant.ts - Add invoice/invoiceLine tenant-scoped query extensions to withTenantContext() - Run migration: 20260304152900_add_invoice_model - Create invoice-service.ts: generateInvoiceNumber, getInvoice, listInvoices, updateInvoiceStatus, markOverdueInvoices, voidInvoice (with JE reversal) - Create credit-service.ts: applyCredit() — DR Subscriber Credits (1150), CR AR (1100) - Create billing-service.ts: generateInvoiceForSubscriber (idempotent, JE + auto-credit), generateMonthlyInvoices (postpaid + prepaid timing with lead days)
This commit is contained in:
@@ -81,6 +81,15 @@ enum JournalEntrySource {
|
||||
MANUAL
|
||||
}
|
||||
|
||||
enum InvoiceStatus {
|
||||
DRAFT
|
||||
SENT
|
||||
PARTIAL
|
||||
PAID
|
||||
OVERDUE
|
||||
VOID
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MODELS
|
||||
// =============================================================================
|
||||
@@ -236,6 +245,8 @@ model Subscriber {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
invoices Invoice[]
|
||||
|
||||
/// Account numbers must be unique within a tenant
|
||||
@@unique([tenantId, accountNumber])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@ -345,3 +356,68 @@ model JournalEntryLine {
|
||||
@@index([accountId])
|
||||
@@index([journalEntryId])
|
||||
}
|
||||
|
||||
/// An Invoice is a billing document issued to a subscriber for a billing period.
|
||||
/// Invoices are generated by the BillingService (auto) or manually.
|
||||
/// Each invoice generation creates a balanced journal entry (DR AR, CR Revenue).
|
||||
/// amountPaid is a transactional convenience field — always updated atomically with JEs.
|
||||
model Invoice {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
/// Auto-generated sequential identifier per tenant (e.g., "INV-2026-0001")
|
||||
invoiceNumber String
|
||||
subscriberId String
|
||||
subscriber Subscriber @relation(fields: [subscriberId], references: [id])
|
||||
/// Billing period start date (inclusive)
|
||||
periodStart DateTime
|
||||
/// Billing period end date (inclusive)
|
||||
periodEnd DateTime
|
||||
/// Payment due date
|
||||
dueDate DateTime
|
||||
/// Subtotal before any adjustments (sum of line totals)
|
||||
subtotal Decimal @db.Decimal(10, 2)
|
||||
/// Total amount due (equals subtotal for now; extensible for taxes/discounts)
|
||||
totalAmount Decimal @db.Decimal(10, 2)
|
||||
/// Amount paid so far — transactional convenience field, NOT a standalone stored balance.
|
||||
/// Always updated atomically with journal entries.
|
||||
amountPaid Decimal @default(0) @db.Decimal(10, 2)
|
||||
status InvoiceStatus @default(DRAFT)
|
||||
/// Journal entry created when this invoice was generated (DR AR, CR Revenue)
|
||||
journalEntryId String?
|
||||
/// Timestamp when invoice was sent to subscriber
|
||||
issuedAt DateTime?
|
||||
/// Timestamp when invoice was fully paid
|
||||
paidAt DateTime?
|
||||
/// Timestamp when invoice was voided
|
||||
voidedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lines InvoiceLine[]
|
||||
|
||||
/// Invoice numbers must be unique within a tenant
|
||||
@@unique([tenantId, invoiceNumber])
|
||||
/// Prevent duplicate invoices for same subscriber + period
|
||||
@@unique([tenantId, subscriberId, periodStart])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, subscriberId])
|
||||
@@index([tenantId, dueDate])
|
||||
}
|
||||
|
||||
/// A single line item on an Invoice (e.g., "50 Mbps Monthly Service — $49.99").
|
||||
model InvoiceLine {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
invoiceId String
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
description String
|
||||
quantity Int @default(1)
|
||||
unitPrice Decimal @db.Decimal(10, 2)
|
||||
lineTotal Decimal @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([invoiceId])
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user