feat(03-02): Collection/Remittance schema, 1030 COA account, migration, tenant scoping

- Added 1030 Cash in Transit to ISP_CHART_OF_ACCOUNTS (between 1020 and 1100)
- Added CollectionStatus (COMPLETED/VOIDED) and RemittanceStatus (PENDING/VERIFIED) enums
- Added Collection model with FIFO allocations, zone-scoped collector, JE link, void fields
- Added CollectionAllocation model linking collections to invoices
- Added Remittance model with two-party verification, variance field, JE link
- Added reverse relations on User (collections, remittances) and Subscriber (collections)
- Applied migration: add-collections-remittances
- Extended TENANT_SCOPED_MODELS with collection, collectionAllocation, remittance
- Added full 12-operation extension blocks for all three new models
This commit is contained in:
kevin-asprec
2026-03-05 07:46:21 +08:00
parent 570218320c
commit 0967fc23fd
4 changed files with 523 additions and 3 deletions

View File

@@ -119,6 +119,16 @@ enum TicketSource {
SUBSCRIBER
}
enum CollectionStatus {
COMPLETED
VOIDED
}
enum RemittanceStatus {
PENDING
VERIFIED
}
// =============================================================================
// MODELS
// =============================================================================
@@ -278,6 +288,7 @@ model Subscriber {
invoices Invoice[]
payments Payment[]
tickets Ticket[]
collections Collection[]
/// Account numbers must be unique within a tenant
@@unique([tenantId, accountNumber])
@@ -362,6 +373,14 @@ model User {
zoneAssignments ZoneAssignment[]
/// Tickets this user created (staff or subscriber portal)
createdTickets Ticket[] @relation("TicketCreatedBy")
/// Collections recorded by this collector
collections Collection[] @relation("CollectionByCollector")
/// Collections voided by this user
voidedCollections Collection[] @relation("CollectionVoidedBy")
/// Remittances this collector submitted
remittances Remittance[] @relation("RemittanceByCollector")
/// Remittances verified by this user (office staff)
verifiedRemittances Remittance[] @relation("RemittanceVerifiedBy")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -473,8 +492,9 @@ model Invoice {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lines InvoiceLine[]
paymentAllocations PaymentAllocation[]
lines InvoiceLine[]
paymentAllocations PaymentAllocation[]
collectionAllocations CollectionAllocation[]
/// Invoice numbers must be unique within a tenant
@@unique([tenantId, invoiceNumber])
@@ -608,6 +628,100 @@ model Ticket {
@@index([subscriberId])
}
/// A Collection records cash received from a subscriber by a field collector.
/// The collector logs the lump sum; FIFO allocation maps it to outstanding invoices.
/// Every collection creates a JE: DR 1030 Cash in Transit, CR 1100 Accounts Receivable.
/// Voids use reversing entries — records are never deleted.
model Collection {
id String @id @default(uuid())
tenantId String
/// The collector who received the cash
collectorId String
collector User @relation("CollectionByCollector", fields: [collectorId], references: [id])
/// The subscriber who paid
subscriberId String
subscriber Subscriber @relation(fields: [subscriberId], references: [id])
/// Total cash received from subscriber
amount Decimal @db.Decimal(10, 2)
/// When the cash was collected (economic date)
collectionDate DateTime
status CollectionStatus @default(COMPLETED)
notes String?
/// Journal entry created when collection was recorded (DR 1030, CR 1100)
journalEntryId String?
/// Timestamp when this collection was voided
voidedAt DateTime?
/// User who voided this collection
voidedById String?
voidedBy User? @relation("CollectionVoidedBy", fields: [voidedById], references: [id])
/// Reversing journal entry created when collection was voided
voidJournalEntryId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
allocations CollectionAllocation[]
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
@@index([tenantId, collectorId])
@@index([tenantId, collectionDate])
@@index([tenantId, subscriberId])
}
/// A CollectionAllocation links a Collection to an Invoice for the allocated amount.
/// Supports partial allocations and FIFO ordering (same pattern as PaymentAllocation).
model CollectionAllocation {
id String @id @default(uuid())
tenantId String
collectionId String
collection Collection @relation(fields: [collectionId], references: [id])
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id])
/// Amount of the collection allocated to this invoice
amount Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
@@index([collectionId])
@@index([invoiceId])
@@index([tenantId])
}
/// A Remittance records a collector turning in collected cash to the office.
/// Two-party verification: collector declares total, office staff counts and verifies.
/// Variance between collector total and staff count is recorded but non-blocking.
/// Verification creates JE: DR 1010 Cash on Hand, CR 1030 Cash in Transit.
model Remittance {
id String @id @default(uuid())
tenantId String
/// The collector remitting cash
collectorId String
collector User @relation("RemittanceByCollector", fields: [collectorId], references: [id])
/// When the remittance occurred
remittanceDate DateTime
/// Total collected by the collector (self-reported)
collectedTotal Decimal @db.Decimal(10, 2)
/// Total counted by office staff (set during verification)
verifiedTotal Decimal? @db.Decimal(10, 2)
/// Difference: verifiedTotal - collectedTotal (positive = overage, negative = shortage)
variance Decimal? @db.Decimal(10, 2)
status RemittanceStatus @default(PENDING)
/// Office staff who verified the remittance
verifiedById String?
verifiedBy User? @relation("RemittanceVerifiedBy", fields: [verifiedById], references: [id])
verifiedAt DateTime?
/// Journal entry created when remittance was verified (DR 1010, CR 1030)
journalEntryId String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
@@index([tenantId, collectorId])
@@index([tenantId, remittanceDate])
@@index([tenantId, status])
}
/// A single line item on an Invoice (e.g., "50 Mbps Monthly Service — $49.99").
model InvoiceLine {
id String @id @default(uuid())