Files
kevin-asprec 816ac5360f docs(02-05): complete payment system plan — Phase 2 complete
Tasks completed: 2/2
- Task 1: Payment model with FIFO allocation and void
- Task 2: Payment APIs, outstanding report, and 29 passing tests

Phase 2 (Subscriber and Billing Core) complete — 265/265 tests passing
SUMMARY: .planning/phases/02-subscriber-and-billing-core/02-05-SUMMARY.md
2026-03-04 23:54:42 +08:00

8.4 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established duration completed
02-subscriber-and-billing-core 05 payments
prisma
postgresql
payment
fifo
journal-entry
double-entry
outstanding-report
idempotency
phase provides
02-04 Invoice model with amountPaid convenience field, BillingService, CreditService
phase provides
02-02 JournalEntryService as sole accounting gateway
phase provides
02-01 ISP COA — accounts 1010/1020/1100/1150/4010
Payment model with idempotency key, FIFO allocation, void fields
PaymentAllocation model linking payments to invoices
PaymentService
recordPayment() FIFO allocation, voidPayment() with reversing JE, getSubscriberPaymentHistory()
OutstandingReportService
getOutstandingReport() with filters/pagination
REST API
POST/GET /api/payments, GET /api/payments/[id], POST /api/payments/[id]/void
REST API
GET /api/subscribers/[id]/payments, GET /api/subscribers/[id]/balance
REST API
GET /api/reports/outstanding
29 integration tests for full payment lifecycle
03-collections-and-routing (collectors receive/record payments)
05-online-payment (payment gateway integration scaffolded here)
reporting (outstanding balances, payment history)
added patterns
FIFO allocation
oldest unpaid invoice (by dueDate ASC) allocated first
Overpayment creates subscriber.creditBalance (atomically with JE, same pattern as amountPaid)
Idempotency via @@unique([tenantId, idempotencyKey]) — safe to retry
Void via reversing JE (immutability — no deletes, corrections only)
PaymentAllocation model tracks allocation breakdown per payment per invoice
created modified
prisma/migrations/20260304154606_add_payment_model/migration.sql
src/lib/services/payment-service.ts
src/lib/services/outstanding-report-service.ts
src/app/api/payments/route.ts
src/app/api/payments/[id]/route.ts
src/app/api/payments/[id]/void/route.ts
src/app/api/subscribers/[id]/payments/route.ts
src/app/api/subscribers/[id]/balance/route.ts
src/app/api/reports/outstanding/route.ts
src/lib/__tests__/payment.test.ts
prisma/schema.prisma
src/lib/prisma-tenant.ts
PaymentAllocation is a separate model (not embedded) — enables per-invoice allocation queries and supports void recalculation
FIFO by dueDate ASC — earliest due date gets first allocation (matches ISP norms)
Overpayment goes to subscriber.creditBalance (same atomically-updated pattern as amountPaid)
Void recalculates invoice.amountPaid by subtracting allocation.amount — safe for multiple-payment scenarios
Outstanding report computes in-process (not raw SQL) — Prisma computed fields not supported; totalAmount-amountPaid computed in JS
Tests use unique invoiceCounter for periodStart — avoids @@unique([tenantId, subscriberId, periodStart]) constraint in test helpers
Payment cleanup order in tests: paymentAllocations -> payments -> invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> users -> tenant
withPermission() HOF pattern continued for all payment/report routes
8min 2026-03-04

Phase 2 Plan 05: Payment System Summary

FIFO cash/bank payment recording with idempotency, partial/full/overpayment tracking, reversing-JE voids, and outstanding balance report — completing the ISP revenue cycle

Performance

  • Duration: 8 min
  • Started: 2026-03-04T15:44:52Z
  • Completed: 2026-03-04T15:52:55Z
  • Tasks: 2/2
  • Files modified: 12

Accomplishments

  • Payment model with FIFO allocation, idempotency key, void support, and journal entry links
  • PaymentService: recordPayment() with FIFO, voidPayment() with reversing JE, getSubscriberPaymentHistory()
  • OutstandingReportService showing who owes what — the core product value
  • 7 REST API routes for payment recording, history, balance, void, and outstanding report
  • 29 integration tests covering all payment scenarios (full/partial/overpayment, FIFO, idempotency, void, tenant isolation)
  • Full regression: 265/265 tests pass

Task Commits

Each task was committed atomically:

  1. Task 1: Payment model + PaymentService with FIFO allocation - 6b91e67 (feat)
  2. Task 2: Payment APIs + outstanding report + tests - bbfc9d6 (feat)

Plan metadata: (created below)

Files Created/Modified

  • prisma/schema.prisma - Added PaymentMethod/PaymentStatus enums, Payment and PaymentAllocation models, relations
  • src/lib/prisma-tenant.ts - Added "payment" and "paymentAllocation" to TENANT_SCOPED_MODELS and withTenantContext()
  • prisma/migrations/20260304154606_add_payment_model/migration.sql - DB migration for Payment and PaymentAllocation tables
  • src/lib/services/payment-service.ts - recordPayment() FIFO, voidPayment() reversing JE, getSubscriberPaymentHistory()
  • src/lib/services/outstanding-report-service.ts - getOutstandingReport() with amount/date/status filters
  • src/app/api/payments/route.ts - POST record payment, GET list payments
  • src/app/api/payments/[id]/route.ts - GET single payment detail
  • src/app/api/payments/[id]/void/route.ts - POST void payment
  • src/app/api/subscribers/[id]/payments/route.ts - GET subscriber payment history
  • src/app/api/subscribers/[id]/balance/route.ts - GET subscriber outstanding balance
  • src/app/api/reports/outstanding/route.ts - GET outstanding balance report
  • src/lib/__tests__/payment.test.ts - 29 integration tests

Decisions Made

  • PaymentAllocation as separate model: Enables per-invoice allocation queries and void recalculation. Each allocation is a row: paymentId + invoiceId + amount.
  • FIFO by dueDate ASC: Oldest due date allocated first — matches standard ISP billing practice.
  • Overpayment to subscriber.creditBalance: Same pattern as invoice.amountPaid — always updated atomically in same transaction as journal entry. CreditService from 02-04 is callable for future credit-to-invoice application.
  • Void recalculation via subtraction: voidPayment() subtracts allocation.amount from invoice.amountPaid — safe even when multiple payments partially cover the same invoice.
  • Outstanding report computed in JS: Prisma does not support computed fields in WHERE/ORDER BY. totalAmount-amountPaid computed in process after fetching invoices. Acceptable for ISP scale (hundreds, not millions of records).
  • Test invoiceCounter for periodStart uniqueness: Invoice schema has @@unique([tenantId, subscriberId, periodStart]). Test helper uses a monotonic counter to generate unique periodStart per invoice, avoiding constraint violations when creating multiple invoices per subscriber in tests.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Fixed unique periodStart constraint in test helper

  • Found during: Task 2 (first test run — 6 tests failed with PrismaClientKnownRequestError)
  • Issue: Test createInvoice() helper used a fixed periodStart: new Date(Date.UTC(2026, 1, 1)) for all invoices. Multiple invoices for the same subscriber violated @@unique([tenantId, subscriberId, periodStart]).
  • Fix: Added invoiceCounter variable; each createInvoice() call uses new Date(Date.UTC(2020, 0, invoiceCounter)) as unique periodStart.
  • Files modified: src/lib/__tests__/payment.test.ts
  • Verification: All 29 tests pass after fix
  • Committed in: bbfc9d6 (Task 2 commit)

Total deviations: 1 auto-fixed (Rule 1 - Bug in test helper) Impact on plan: Minimal. Test helper bug; no production code affected. Fix is a standard test-isolation technique.

Issues Encountered

None beyond the test helper bug documented above.

User Setup Required

None - no external service configuration required.

Next Phase Readiness

  • Phase 2 (Subscriber and Billing Core) is complete: COA, JournalEntryService, Subscriber, BillingEngine, PaymentService all done.
  • 265 total tests passing across all Phase 1 and Phase 2 plans.
  • Phase 3 (Collections and Routing) can begin: collector routes, payment collection, zone-based assignment.
  • Key context for Phase 3: collectors will use PaymentService.recordPayment() to record collected payments; subscriber.zone field is already on the schema.

Phase: 02-subscriber-and-billing-core Completed: 2026-03-04