Phase 02: Subscriber and Billing Core - 5 plans in 4 waves - Wave 1: 02-01 (COA), 02-03 (Subscribers) parallel - Wave 2: 02-02 (Journal Entry Service) - Wave 3: 02-04 (Billing Engine) - Wave 4: 02-05 (Payment Tracker) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-subscriber-and-billing-core | 05 | execute | 4 |
|
|
true |
|
Purpose: This completes the revenue cycle: subscribers get invoices (02-04), and now those invoices can be paid. The outstanding report gives ISP owners the financial visibility that is the core product value. Output: Payment model, PaymentService with FIFO allocation, void with reversing entries, subscriber payment history, outstanding balance report, all with tests.
<execution_context> @C:\Users\KevinAsprec.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-subscriber-and-billing-core/02-CONTEXT.md @.planning/phases/02-subscriber-and-billing-core/02-02-SUMMARY.md @.planning/phases/02-subscriber-and-billing-core/02-03-SUMMARY.md @.planning/phases/02-subscriber-and-billing-core/02-04-SUMMARY.md @prisma/schema.prisma @src/lib/accounting/journal-entry-service.ts @src/lib/services/invoice-service.ts @src/lib/services/subscriber-service.ts @src/lib/prisma-tenant.ts Task 1: Payment model + PaymentService with FIFO allocation prisma/schema.prisma src/lib/services/payment-service.ts src/lib/prisma-tenant.ts prisma/migrations/*_add_payment_model/migration.sql 1. Add enums to prisma/schema.prisma: - `PaymentMethod`: CASH, BANK_TRANSFER - `PaymentStatus`: COMPLETED, VOIDED-
Add
Paymentmodel:- id (uuid), tenantId (String), subscriberId (String, relation to Subscriber), amount (Decimal, precision 10 scale 2), paymentMethod (PaymentMethod), referenceNumber (String? — bank transfer reference, receipt number), paymentDate (DateTime), notes (String?), status (PaymentStatus, default COMPLETED), idempotencyKey (String — caller-provided unique key to prevent double-recording), journalEntryId (String? — links to the JE created), voidedAt (DateTime?), voidedById (String?), voidJournalEntryId (String? — the reversing JE), recordedById (String, relation to User — who recorded it), createdAt, updatedAt
- @@unique([tenantId, idempotencyKey]) — idempotency enforcement
- @@index([tenantId]), @@index([tenantId, subscriberId]), @@index([tenantId, paymentDate])
-
Add
PaymentAllocationmodel (tracks which invoices a payment was applied to):- id (uuid), tenantId (String), paymentId (String, relation to Payment), invoiceId (String, relation to Invoice), amount (Decimal, precision 10 scale 2), createdAt
- @@index([paymentId]), @@index([invoiceId])
-
Update TENANT_SCOPED_MODELS in prisma-tenant.ts to include "payment" and "paymentAllocation". Add query extensions.
-
Create src/lib/services/payment-service.ts:
a.
recordPayment(tenantPrisma, { subscriberId, amount, paymentMethod, referenceNumber?, paymentDate, notes?, idempotencyKey, recordedById }):- Check idempotency: if payment with this idempotencyKey already exists, return the existing payment (not an error)
- Validate amount > 0
- Validate subscriber exists
FIFO allocation (all in one transaction):
- Check subscriber.creditBalance > 0? If yes, include it in available amount.
- Find all unpaid/partial invoices for this subscriber, ordered by dueDate ASC (oldest first)
- Allocate payment amount to invoices FIFO:
- For each invoice: remaining = invoice.totalAmount - invoice.amountPaid
- Allocate min(availableAmount, remaining) to this invoice
- Create PaymentAllocation record
- Update invoice.amountPaid += allocated
- If invoice fully paid: update status to PAID, set paidAt
- If invoice partially paid: update status to PARTIAL
- Reduce availableAmount by allocated
- Stop when availableAmount reaches 0
- If amount left over after all invoices: update subscriber.creditBalance += leftover
- Create journal entry via JournalEntryService.createEntry:
- For CASH: Debit Cash on Hand (1010), Credit AR (1100)
- For BANK_TRANSFER: Debit Cash in Bank (1020), Credit AR (1100)
- If overpayment exists: also Credit Subscriber Credits (1150) for overpayment portion
- source: SYSTEM, referenceType: "Payment", referenceId: payment.id
- Link journalEntryId to payment
- Return payment with allocations
b.
voidPayment(tenantPrisma, paymentId, voidedById):- Find payment with allocations
- Verify status is COMPLETED (not already voided)
- Reverse allocations: for each PaymentAllocation, reduce invoice.amountPaid, recalculate invoice status (PAID->PARTIAL or PARTIAL->SENT/OVERDUE)
- If subscriber.creditBalance was increased by overpayment, reduce it
- Create reversing journal entry via JournalEntryService.reverseEntry
- Update payment: status=VOIDED, voidedAt, voidedById, voidJournalEntryId
- All in one transaction
- Return voided payment
c.
getSubscriberPaymentHistory(tenantPrisma, subscriberId, { page?, pageSize? }):- Return all payments for subscriber, ordered by paymentDate desc, with allocations and linked invoices
- Paginated
d.
applyCredit(tenantPrisma, subscriberId, invoiceId):- If subscriber.creditBalance > 0, apply it to the specified invoice
- Create PaymentAllocation, update invoice, reduce creditBalance
- Called automatically by billing service when generating new invoices for subscribers with credit
-
Run
npx prisma migrate dev --name add_payment_modelnpx prisma migrate status— no pendingnpx prisma generatesucceedsnpx tsc --noEmit— clean Payment and PaymentAllocation models exist. PaymentService handles FIFO allocation, overpayment credit, void with reversing entries, and idempotency.
-
Create API routes:
a. POST /api/payments — record a payment. withPermission("create", "Payment"). Accepts { subscriberId, amount, paymentMethod, referenceNumber?, paymentDate, notes?, idempotencyKey }. Calls recordPayment. Returns 201 with payment and allocations.
b. GET /api/payments — list payments. withPermission("read", "Payment"). Accepts ?subscriberId=&startDate=&endDate=&method=&page=&pageSize=. Returns paginated payments.
c. GET /api/payments/[id] — get payment detail with allocations. withPermission("read", "Payment").
d. POST /api/payments/[id]/void — void a payment. withPermission("manage", "Payment") (Admin/Office Staff only). Calls voidPayment. Returns voided payment.
e. GET /api/subscribers/[id]/payments — subscriber payment history. withPermission("read", "Payment"). Calls getSubscriberPaymentHistory. Returns paginated history.
f. GET /api/subscribers/[id]/balance — subscriber outstanding balance. withPermission("read", "Payment"). Returns { subscriberId, totalOutstanding, creditBalance, invoicesSummary }.
g. GET /api/reports/outstanding — outstanding balance report. withPermission("read", "Report"). Accepts ?startDate=&endDate=&status=&minAmount=&maxAmount=&page=&pageSize=. Returns report with totals.
-
Write comprehensive tests in src/lib/tests/payment.test.ts:
Payment recording tests:
- Record full payment against single invoice: invoice status -> PAID
- Record partial payment: invoice status -> PARTIAL, amountPaid updated
- Record payment larger than invoice amount: overpayment creates credit balance
- FIFO allocation: payment applied to oldest invoice first
- Multiple partial payments accumulate on same invoice
- Journal entry created: debit Cash (1010 for CASH), credit AR (1100)
- Journal entry balanced (debits = credits)
- Payment with BANK_TRANSFER debits Cash in Bank (1020)
Idempotency tests:
- Same idempotencyKey returns existing payment, not duplicate
- Different idempotencyKey creates new payment
Credit balance tests:
- Overpayment increases subscriber.creditBalance
- Credit balance auto-applied to next invoice
Void tests:
- Void payment reverses allocations (invoice.amountPaid decreases)
- Void payment creates reversing journal entry
- Void payment reduces credit balance if overpayment existed
- Cannot void already-voided payment
- Invoice status recalculated after void (PAID -> reverts appropriately)
Outstanding report tests:
- Report shows only unpaid invoices (SENT, PARTIAL, OVERDUE)
- Outstanding = totalAmount - amountPaid
- Filter by date range works
- Filter by minimum amount works
- Total outstanding sums correctly
- Report excludes PAID and VOID invoices
Payment history tests:
- Subscriber payment history shows all payments with allocations
- History ordered by date descending
- Subscriber balance shows correct outstanding amount
Tenant isolation:
- Payment from Tenant A not visible to Tenant B
Run: npx vitest run src/lib/__tests__/payment.test.ts
- npx vitest run src/lib/__tests__/payment.test.ts — all tests pass
- npx vitest run — ALL tests pass (full suite regression)
- npx tsc --noEmit — clean
- POST /api/payments with valid data returns 201 with FIFO allocations
- POST /api/payments with same idempotencyKey returns existing payment
- POST /api/payments/{id}/void reverses journal entry and allocations
- GET /api/reports/outstanding returns correct outstanding balances
- GET /api/subscribers/{id}/payments returns payment history
Staff can record payments with FIFO allocation. Partial, full, and overpayments all handled correctly. Every payment has a balanced journal entry. Voids use reversing entries. Outstanding report derives balances from journal. Subscriber payment history available. Idempotency enforced. All tests pass.
<success_criteria>
- Payment model with idempotencyKey, method, allocations
- FIFO allocation to oldest unpaid invoice
- Partial payments -> PARTIAL status, full -> PAID
- Overpayment -> subscriber credit balance
- Every payment creates balanced journal entry (debit Cash/Bank, credit AR)
- Void creates reversing journal entry, recalculates invoice status
- Idempotency key prevents double-recording
- Outstanding report shows correct balances filtered by date/status/amount
- Subscriber payment history shows all transactions
- Tenant isolation enforced
- All tests pass </success_criteria>