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>
13 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 | 03 | execute | 1 |
|
true |
|
Purpose: Subscribers are the core business entity — every billing, payment, and collection operation targets subscribers. The billing engine (02-04) needs Subscriber and ServicePlan to generate invoices. Output: Subscriber and ServicePlan Prisma models, service layer, API routes, search/filter, status lifecycle, 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 @prisma/schema.prisma @src/lib/prisma-tenant.ts @src/lib/middleware/authorize.ts Task 1: Subscriber and ServicePlan Prisma models + service layer prisma/schema.prisma src/lib/services/subscriber-service.ts src/lib/services/service-plan-service.ts src/lib/prisma-tenant.ts prisma/migrations/*_add_subscriber_models/migration.sql 1. Add enums to prisma/schema.prisma: - `SubscriberStatus`: ACTIVE, SUSPENDED, CANCELLED - `BillingType`: PREPAID, POSTPAID-
Add
ServicePlanmodel:- id (uuid), tenantId (String), name (String), speed (String — e.g., "50 Mbps"), monthlyPrice (Decimal, precision 10 scale 2), billingType (BillingType), description (String?), isActive (Boolean, default true — soft-delete plans), createdAt, updatedAt
- @@unique([tenantId, name]) — plan names unique per tenant
- @@index([tenantId])
-
Add
Subscribermodel:- id (uuid), tenantId (String), accountNumber (String — auto-generated, e.g., "SUB-0001"), firstName (String), lastName (String), email (String?), phone (String?), address (String), zone (String? — for collector routing in Phase 3), servicePlanId (String, relation to ServicePlan), status (SubscriberStatus, default ACTIVE), billingDay (Int — day of month for invoice generation, derived from signup date), activatedAt (DateTime, default now()), suspendedAt (DateTime?), cancelledAt (DateTime?), autoSuspendDays (Int? — per-subscriber override, falls back to tenant setting), notes (String?), creditBalance (Decimal, precision 10 scale 2, default 0 — this tracks subscriber credit from overpayments, NOT an account balance; it's a convenience field that's always updated transactionally with payment journal entries), createdAt, updatedAt
- @@unique([tenantId, accountNumber])
- @@index([tenantId])
- @@index([tenantId, status])
- @@index([tenantId, servicePlanId])
IMPORTANT on creditBalance: This is NOT a "stored balance" in the accounting sense. All financial balances come from the journal. This field tracks overpayment credits for the FIFO allocation system (02-05). It is always updated atomically within the same transaction as the journal entry that changes it. The CONTEXT.md decision "no mutable balance fields" refers to account/ledger balances, not operational convenience fields.
-
Add
TenantSettingsmodel (or add fields to Tenant model — prefer separate model for extensibility):- id (uuid), tenantId (String, @unique — one settings record per tenant), autoSuspendDays (Int, default 30 — days overdue before auto-suspension), prepaidLeadDays (Int, default 7 — days before billing date to generate prepaid invoices), createdAt, updatedAt
- @@index([tenantId])
-
Update TENANT_SCOPED_MODELS in prisma-tenant.ts to include "subscriber", "servicePlan", "tenantSettings". Add query extension blocks.
-
Create src/lib/services/service-plan-service.ts:
createServicePlan(tenantPrisma, { name, speed, monthlyPrice, billingType, description? })— validates name not empty, price > 0. Returns created plan.updateServicePlan(tenantPrisma, planId, updates)— partial update. Returns updated plan.listServicePlans(tenantPrisma, { activeOnly? })— returns plans, ordered by name. Default activeOnly=true.deactivateServicePlan(tenantPrisma, planId)— sets isActive=false. Does NOT delete (subscribers may reference it).
-
Create src/lib/services/subscriber-service.ts:
generateAccountNumber(tenantPrisma)— query max accountNumber for tenant, increment. Format: "SUB-{NNNN}" starting at SUB-0001.createSubscriber(tenantPrisma, { firstName, lastName, email?, phone?, address, zone?, servicePlanId, notes? })— validates required fields, verifies servicePlanId exists and is active, generates accountNumber, sets billingDay from current date (day of month, capped at 28 to avoid month-length issues). Returns created subscriber.updateSubscriber(tenantPrisma, subscriberId, updates)— partial update of profile fields (not status — status changes go through changeSubscriberStatus). Returns updated subscriber.changeSubscriberStatus(tenantPrisma, subscriberId, newStatus, reason?):- ACTIVE -> SUSPENDED: set suspendedAt, clear cancelledAt
- ACTIVE -> CANCELLED: set cancelledAt
- SUSPENDED -> ACTIVE: clear suspendedAt (reactivation — caller must verify outstanding balance is zero, enforced in 02-05)
- SUSPENDED -> CANCELLED: set cancelledAt
- CANCELLED -> ACTIVE: clear cancelledAt, clear suspendedAt (reversible cancellation per CONTEXT.md)
- Returns updated subscriber
searchSubscribers(tenantPrisma, { status?, servicePlanId?, search?, page?, pageSize? })— search by name (firstName or lastName contains), filter by status and plan. Paginated. Returns { subscribers, total, page, pageSize }.getSubscriber(tenantPrisma, subscriberId)— get single subscriber with servicePlan included.
-
Run
npx prisma migrate dev --name add_subscriber_modelsnpx prisma migrate status— no pendingnpx prisma generatesucceedsnpx tsc --noEmit— clean Subscriber and ServicePlan models in database. Service layer handles CRUD, status lifecycle, search/filter. TenantSettings model for auto-suspend configuration.
a. GET /api/service-plans — list service plans. withPermission("read", "Subscriber"). Accepts ?activeOnly=true|false. Returns plans array. b. POST /api/service-plans — create plan. withPermission("manage", "Subscriber"). Accepts { name, speed, monthlyPrice, billingType, description? }. Returns 201 with created plan. c. PUT /api/service-plans/[id] — update plan. withPermission("manage", "Subscriber"). Accepts partial fields. Returns updated plan.
d. GET /api/subscribers — list/search subscribers. withPermission("read", "Subscriber"). Accepts ?status=&servicePlanId=&search=&page=&pageSize=. Returns paginated results. e. POST /api/subscribers — register subscriber. withPermission("manage", "Subscriber"). Accepts { firstName, lastName, email?, phone?, address, zone?, servicePlanId, notes? }. Returns 201 with created subscriber. f. GET /api/subscribers/[id] — get subscriber detail with plan. withPermission("read", "Subscriber"). g. PUT /api/subscribers/[id] — update subscriber profile. withPermission("manage", "Subscriber"). Returns updated subscriber. h. PATCH /api/subscribers/[id]/status — change subscriber status. withPermission("manage", "Subscriber"). Accepts { status: "ACTIVE"|"SUSPENDED"|"CANCELLED", reason? }. Returns updated subscriber.
-
Write tests in src/lib/tests/subscriber.test.ts:
ServicePlan tests:
- Create plan with valid data succeeds
- Create plan with duplicate name fails
- Create plan with zero/negative price fails
- List plans returns only active by default
- Deactivate plan sets isActive=false
Subscriber CRUD tests:
- Register subscriber with all fields succeeds, accountNumber auto-generated
- Register subscriber with invalid servicePlanId fails
- Register subscriber sets billingDay from signup date
- Update subscriber profile fields
- Get subscriber includes servicePlan relation
- Search by name (partial match)
- Filter by status
- Filter by servicePlanId
- Pagination works correctly (page, pageSize, total)
Status lifecycle tests:
- ACTIVE -> SUSPENDED sets suspendedAt
- ACTIVE -> CANCELLED sets cancelledAt
- SUSPENDED -> ACTIVE clears suspendedAt
- SUSPENDED -> CANCELLED sets cancelledAt
- CANCELLED -> ACTIVE clears both timestamps (reversible)
- Account number format: SUB-0001, SUB-0002, etc.
Tenant isolation:
- Subscriber from Tenant A not visible to Tenant B
Run: npx vitest run src/lib/__tests__/subscriber.test.ts
- npx vitest run src/lib/__tests__/subscriber.test.ts — all tests pass
- npx tsc --noEmit — clean
- POST /api/subscribers with valid data returns 201 with accountNumber
- GET /api/subscribers?status=ACTIVE returns only active subscribers
- PATCH /api/subscribers/{id}/status transitions correctly
Staff can register subscribers, manage service plans, change subscriber status through full lifecycle, search and filter subscribers. All tests pass. Tenant isolation verified.
<success_criteria>
- ServicePlan model with name, speed, monthlyPrice, billingType
- Subscriber model with accountNumber, status lifecycle, billingDay, plan reference
- TenantSettings model with autoSuspendDays and prepaidLeadDays
- All status transitions work (including reversible cancellation)
- Search by name, filter by status/plan, pagination
- Account numbers auto-generated sequentially per tenant
- billingDay set from signup date (capped at 28)
- Tenant isolation enforced
- All tests pass </success_criteria>