Files
kevin-asprec 5c6969343b docs(02-03): complete Subscriber and ServicePlan management plan
Tasks completed: 2/2
- Task 1: Subscriber and ServicePlan Prisma models + service layer
- Task 2: Subscriber and ServicePlan API routes + tests

SUMMARY: .planning/phases/02-subscriber-and-billing-core/02-03-SUMMARY.md
2026-03-04 23:04:15 +08:00

7.6 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 03 database
prisma
postgresql
subscriber
billing
service-plans
tenant-isolation
phase provides
02-subscriber-and-billing-core/02-01 Prisma setup, withTenantContext HOF, COA models — established patterns reused here
phase provides
01-foundation/01-03 prisma-tenant.ts TENANT_SCOPED_MODELS pattern extended with new models
phase provides
01-foundation/01-04 withPermission HOF wrapping API routes
Subscriber Prisma model with accountNumber, status lifecycle, billingDay, creditBalance
ServicePlan Prisma model with name, speed, monthlyPrice, billingType, soft-delete
TenantSettings Prisma model with autoSuspendDays and prepaidLeadDays
subscriber-service.ts with createSubscriber, updateSubscriber, changeSubscriberStatus, searchSubscribers, getSubscriber
service-plan-service.ts with createServicePlan, updateServicePlan, listServicePlans, deactivateServicePlan
Full REST API for subscribers and service plans
41 integration tests (162 total passing)
02-04-billing-engine (Subscriber and ServicePlan are the billing targets)
02-05-payment-allocation (creditBalance field on Subscriber for FIFO allocation)
03-collector-app (zone field on Subscriber for collector routing)
04-inventory (subscriber association)
added patterns
Closure pattern for dynamic route params with withPermission HOF
as any cast for Prisma create() data when tenantId injected by extension
Soft-delete pattern via isActive boolean on ServicePlan
Reversible status lifecycle with explicit transition validation matrix
created modified
prisma/migrations/20260304145633_add_subscriber_models/migration.sql
src/lib/services/service-plan-service.ts
src/lib/services/subscriber-service.ts
src/app/api/service-plans/route.ts
src/app/api/service-plans/[id]/route.ts
src/app/api/subscribers/route.ts
src/app/api/subscribers/[id]/route.ts
src/app/api/subscribers/[id]/status/route.ts
src/lib/__tests__/subscriber.test.ts
prisma/schema.prisma
src/lib/prisma-tenant.ts
creditBalance on Subscriber is NOT a ledger balance — it is an operational convenience field for FIFO overpayment credit allocation (02-05), always updated atomically with journal entries
billingDay capped at 28 — avoids month-length issues (no SUB-0001 billed on Feb 29 that doesn't exist)
CANCELLED -> ACTIVE reversible by design (per CONTEXT.md) — ISPs frequently reinstate cancelled accounts
Closure pattern for dynamic params — withPermission HOF signature doesn't pass params; outer fn receives them from Next.js then inner handler uses via closure (same as accounting/periods/[id]/close pattern)
as any cast in create() calls — Prisma static type requires tenantId but withTenantContext() extension injects it at runtime; cast is intentional and safe
Closure pattern for dynamic params: export function PUT(req, { params }) { return withPermission(...)(async (req, { user }) => { const { id } = await params; ... })(req); }
Service layer takes TenantPrisma (ReturnType<typeof withTenantContext>) — never takes tenantId directly, always scoped client
Status transition matrix: Record<SubscriberStatus, SubscriberStatus[]> — explicit, exhaustive, easily auditable
generateAccountNumber queries max accountNumber and increments — sequential per tenant, format SUB-NNNN
7min 2026-03-04

Phase 2 Plan 03: Subscriber and ServicePlan Management Summary

Subscriber and ServicePlan Prisma models with full CRUD, status lifecycle (ACTIVE/SUSPENDED/CANCELLED all reversible), sequential account numbers (SUB-0001+), billingDay from signup date, and paginated search/filter — the core billing targets for 02-04

Performance

  • Duration: 7 min
  • Started: 2026-03-04T14:55:45Z
  • Completed: 2026-03-04T15:02:45Z
  • Tasks: 2/2
  • Files modified: 11

Accomplishments

  • Subscriber and ServicePlan Prisma models with full migration applied to database
  • TenantSettings model for per-tenant autoSuspendDays and prepaidLeadDays configuration
  • Complete service layer: subscriber CRUD, status lifecycle transitions, plan CRUD with soft-delete
  • Full REST API (7 endpoints) with closure pattern for dynamic routes
  • 41 new integration tests covering all lifecycle transitions, search/filter, pagination, and tenant isolation

Task Commits

Each task was committed atomically:

  1. Task 1: Subscriber and ServicePlan Prisma models + service layer - 9cc6af1 (feat)
  2. Task 2: Subscriber and ServicePlan API routes + tests - 03a4a29 (feat)

Plan metadata: (docs commit follows)

Files Created/Modified

  • prisma/schema.prisma - Added SubscriberStatus, BillingType enums; ServicePlan, Subscriber, TenantSettings models
  • prisma/migrations/20260304145633_add_subscriber_models/migration.sql - Database migration
  • src/lib/prisma-tenant.ts - Extended TENANT_SCOPED_MODELS and query blocks for subscriber, servicePlan, tenantSettings
  • src/lib/services/service-plan-service.ts - createServicePlan, updateServicePlan, listServicePlans, deactivateServicePlan
  • src/lib/services/subscriber-service.ts - createSubscriber, updateSubscriber, changeSubscriberStatus, searchSubscribers, getSubscriber, generateAccountNumber
  • src/app/api/service-plans/route.ts - GET (list with activeOnly filter), POST (create)
  • src/app/api/service-plans/[id]/route.ts - PUT (partial update)
  • src/app/api/subscribers/route.ts - GET (paginated search with filters), POST (register)
  • src/app/api/subscribers/[id]/route.ts - GET (with servicePlan relation), PUT (profile update)
  • src/app/api/subscribers/[id]/status/route.ts - PATCH (status lifecycle transitions)
  • src/lib/__tests__/subscriber.test.ts - 41 integration tests

Decisions Made

  • creditBalance as operational convenience field — Distinguishing from "no mutable balance fields" rule: creditBalance tracks overpayment credits for FIFO allocation (02-05), always updated atomically with journal entries. Architectural mutable balance fields (account balances) remain journal-derived.
  • billingDay capped at 28 — Subscribers who sign up on day 29, 30, or 31 get billingDay=28 to avoid month-length boundary issues with invoice generation.
  • Reversible CANCELLED -> ACTIVE transition — Per CONTEXT.md, ISPs frequently reinstate cancelled accounts. No one-way door on cancellation.
  • Closure pattern for dynamic paramswithPermission HOF doesn't pass Next.js route params to handlers. Using closure pattern (same as accounting/periods/[id]/close) instead of modifying the HOF.
  • as any cast in create() calls — The Prisma extended client injects tenantId at runtime but static types still require it. Safe to cast since the extension guarantees injection.

Deviations from Plan

None — plan executed exactly as written.

Issues Encountered

None.

User Setup Required

None — no external service configuration required.

Next Phase Readiness

  • Subscriber and ServicePlan models ready for 02-04 billing engine (invoice generation targets)
  • creditBalance field on Subscriber ready for 02-05 FIFO payment allocation
  • zone field on Subscriber ready for Phase 3 collector routing
  • TenantSettings provides autoSuspendDays for auto-suspension logic in billing engine
  • All tenant isolation verified: Subscriber/ServicePlan data not visible across tenants

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