--- phase: 02-subscriber-and-billing-core plan: "03" type: execute wave: 1 depends_on: [] files_modified: - prisma/schema.prisma - src/lib/services/subscriber-service.ts - src/lib/services/service-plan-service.ts - src/app/api/subscribers/route.ts - src/app/api/subscribers/[id]/route.ts - src/app/api/subscribers/[id]/status/route.ts - src/app/api/service-plans/route.ts - src/app/api/service-plans/[id]/route.ts - src/lib/prisma-tenant.ts - prisma/migrations/*_add_subscriber_models/migration.sql - src/lib/__tests__/subscriber.test.ts autonomous: true must_haves: truths: - "Staff can register a subscriber with name, address, contact info, and plan assignment" - "Subscriber has status lifecycle: Active, Suspended, Cancelled — all transitions are valid" - "Staff can search and filter subscribers by status, plan, and name" - "Service plans have name, speed, monthly price, and billing type (prepaid/postpaid)" - "Each subscriber has a billingDay derived from their signup date (anniversary billing)" artifacts: - path: "prisma/schema.prisma" provides: "Subscriber, ServicePlan models with tenant scoping" contains: "model Subscriber" - path: "src/lib/services/subscriber-service.ts" provides: "Subscriber CRUD, status transitions, search/filter" exports: ["createSubscriber", "updateSubscriber", "changeSubscriberStatus", "searchSubscribers"] - path: "src/lib/services/service-plan-service.ts" provides: "Service plan CRUD" exports: ["createServicePlan", "updateServicePlan", "listServicePlans"] - path: "src/app/api/subscribers/route.ts" provides: "GET (list/search) and POST (create) subscriber endpoints" - path: "src/app/api/service-plans/route.ts" provides: "GET (list) and POST (create) service plan endpoints" key_links: - from: "src/lib/services/subscriber-service.ts" to: "prisma/schema.prisma" via: "Subscriber CRUD with tenant scoping" pattern: "subscriber\\.(create|findMany|update)" - from: "src/app/api/subscribers/route.ts" to: "src/lib/services/subscriber-service.ts" via: "Route handlers call service functions" pattern: "createSubscriber|searchSubscribers" - from: "prisma/schema.prisma" to: "prisma/schema.prisma" via: "Subscriber.servicePlanId references ServicePlan.id" pattern: "servicePlanId" --- Build subscriber management and service plan CRUD. Staff can register subscribers with all required details and a plan assignment. Subscribers have a status lifecycle (Active/Suspended/Cancelled) with transitions. Service plans define name, speed, monthly price, and billing type (prepaid vs postpaid). Search and filtering by status, plan, and name. 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. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.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 2. Add `ServicePlan` model: - 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]) 3. Add `Subscriber` model: - 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. 4. Add `TenantSettings` model (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]) 5. Update TENANT_SCOPED_MODELS in prisma-tenant.ts to include "subscriber", "servicePlan", "tenantSettings". Add query extension blocks. 6. 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). 7. 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. 8. Run `npx prisma migrate dev --name add_subscriber_models` - `npx prisma migrate status` — no pending - `npx prisma generate` succeeds - `npx tsc --noEmit` — clean Subscriber and ServicePlan models in database. Service layer handles CRUD, status lifecycle, search/filter. TenantSettings model for auto-suspend configuration. Task 2: Subscriber and ServicePlan API routes + tests src/app/api/subscribers/route.ts src/app/api/subscribers/[id]/route.ts src/app/api/subscribers/[id]/status/route.ts src/app/api/service-plans/route.ts src/app/api/service-plans/[id]/route.ts src/lib/__tests__/subscriber.test.ts 1. Create API routes: 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. 2. 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. - `npx vitest run` — all existing + new tests pass - `npx tsc --noEmit` — clean - Full CRUD cycle: create plan -> create subscriber with plan -> search -> update -> change status - Subscriber from Tenant A invisible to Tenant B - Status transitions follow defined lifecycle - 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 After completion, create `.planning/phases/02-subscriber-and-billing-core/02-03-SUMMARY.md`