feat(02-05): Payment model with FIFO allocation and void
- Add PaymentMethod (CASH, BANK_TRANSFER) and PaymentStatus (COMPLETED, VOIDED) enums - Add Payment model with idempotency key, journal entry link, void fields - Add PaymentAllocation model for FIFO invoice allocation tracking - Add Payment/PaymentAllocation relations to Subscriber, Invoice, User - Update TENANT_SCOPED_MODELS with "payment" and "paymentAllocation" - Add payment/paymentAllocation query extensions in withTenantContext() - Implement recordPayment() with FIFO allocation, overpayment credit balance - Implement voidPayment() with reversing journal entries - Implement getSubscriberPaymentHistory() with pagination - Run migration: 20260304154606_add_payment_model
This commit is contained in:
@@ -90,6 +90,16 @@ enum InvoiceStatus {
|
||||
VOID
|
||||
}
|
||||
|
||||
enum PaymentMethod {
|
||||
CASH
|
||||
BANK_TRANSFER
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
COMPLETED
|
||||
VOIDED
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MODELS
|
||||
// =============================================================================
|
||||
@@ -246,6 +256,7 @@ model Subscriber {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
invoices Invoice[]
|
||||
payments Payment[]
|
||||
|
||||
/// Account numbers must be unique within a tenant
|
||||
@@unique([tenantId, accountNumber])
|
||||
@@ -282,6 +293,8 @@ model User {
|
||||
createdJournalEntries JournalEntry[] @relation("JournalEntryCreatedBy")
|
||||
/// Journal entries this user approved (checker)
|
||||
approvedJournalEntries JournalEntry[] @relation("JournalEntryApprovedBy")
|
||||
/// Payments this user recorded
|
||||
recordedPayments Payment[] @relation("PaymentRecordedBy")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -393,7 +406,8 @@ model Invoice {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lines InvoiceLine[]
|
||||
lines InvoiceLine[]
|
||||
paymentAllocations PaymentAllocation[]
|
||||
|
||||
/// Invoice numbers must be unique within a tenant
|
||||
@@unique([tenantId, invoiceNumber])
|
||||
@@ -406,6 +420,70 @@ model Invoice {
|
||||
@@index([tenantId, dueDate])
|
||||
}
|
||||
|
||||
/// A Payment records a cash or bank transfer received from a subscriber.
|
||||
/// Payments are allocated FIFO to oldest unpaid invoices.
|
||||
/// Every payment creates a balanced journal entry (DR Cash/Bank, CR AR).
|
||||
/// Voids use reversing entries — records are never deleted.
|
||||
/// idempotencyKey prevents double-recording; @@unique([tenantId, idempotencyKey]).
|
||||
model Payment {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
subscriberId String
|
||||
subscriber Subscriber @relation(fields: [subscriberId], references: [id])
|
||||
/// Total amount received
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
paymentMethod PaymentMethod
|
||||
/// Optional external reference (e.g., bank reference number, receipt number)
|
||||
referenceNumber String?
|
||||
/// When the payment was received (economic date, not necessarily createdAt)
|
||||
paymentDate DateTime
|
||||
notes String?
|
||||
status PaymentStatus @default(COMPLETED)
|
||||
/// Client-supplied key to prevent double-recording on retries
|
||||
idempotencyKey String
|
||||
/// Journal entry created when payment was recorded (DR Cash/Bank, CR AR)
|
||||
journalEntryId String?
|
||||
/// Timestamp when this payment was voided
|
||||
voidedAt DateTime?
|
||||
/// User who voided this payment
|
||||
voidedById String?
|
||||
/// Reversing journal entry created when payment was voided
|
||||
voidJournalEntryId String?
|
||||
/// User who recorded this payment
|
||||
recordedById String
|
||||
recordedBy User @relation("PaymentRecordedBy", fields: [recordedById], references: [id])
|
||||
|
||||
allocations PaymentAllocation[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// Idempotency: one payment per key per tenant
|
||||
@@unique([tenantId, idempotencyKey])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, subscriberId])
|
||||
@@index([tenantId, paymentDate])
|
||||
}
|
||||
|
||||
/// A PaymentAllocation links a Payment to an Invoice for the allocated amount.
|
||||
/// Supports partial allocations and FIFO ordering.
|
||||
model PaymentAllocation {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
paymentId String
|
||||
payment Payment @relation(fields: [paymentId], references: [id])
|
||||
invoiceId String
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
/// Amount of the payment allocated to this invoice
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([paymentId])
|
||||
@@index([invoiceId])
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
/// A single line item on an Invoice (e.g., "50 Mbps Monthly Service — $49.99").
|
||||
model InvoiceLine {
|
||||
id String @id @default(uuid())
|
||||
|
||||
Reference in New Issue
Block a user