--- phase: 02-subscriber-and-billing-core plan: "03" subsystem: database tags: [prisma, postgresql, subscriber, billing, service-plans, tenant-isolation] # Dependency graph requires: - phase: 02-subscriber-and-billing-core/02-01 provides: Prisma setup, withTenantContext HOF, COA models — established patterns reused here - phase: 01-foundation/01-03 provides: prisma-tenant.ts TENANT_SCOPED_MODELS pattern extended with new models - phase: 01-foundation/01-04 provides: withPermission HOF wrapping API routes provides: - 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) affects: - 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) # Tech tracking tech-stack: 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 key-files: created: - 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 modified: - prisma/schema.prisma - src/lib/prisma-tenant.ts key-decisions: - "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" patterns-established: - "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) — never takes tenantId directly, always scoped client" - "Status transition matrix: Record — explicit, exhaustive, easily auditable" - "generateAccountNumber queries max accountNumber and increments — sequential per tenant, format SUB-NNNN" # Metrics duration: 7min completed: 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 params** — `withPermission` 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*