Files
NetForge/.planning/phases/02-subscriber-and-billing-core/02-03-PLAN.md
kevin-asprec 9489b5c7bc docs(02): create phase plan
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>
2026-03-04 22:27:35 +08:00

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
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
true
truths artifacts key_links
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)
path provides contains
prisma/schema.prisma Subscriber, ServicePlan models with tenant scoping model Subscriber
path provides exports
src/lib/services/subscriber-service.ts Subscriber CRUD, status transitions, search/filter
createSubscriber
updateSubscriber
changeSubscriberStatus
searchSubscribers
path provides exports
src/lib/services/service-plan-service.ts Service plan CRUD
createServicePlan
updateServicePlan
listServicePlans
path provides
src/app/api/subscribers/route.ts GET (list/search) and POST (create) subscriber endpoints
path provides
src/app/api/service-plans/route.ts GET (list) and POST (create) service plan endpoints
from to via pattern
src/lib/services/subscriber-service.ts prisma/schema.prisma Subscriber CRUD with tenant scoping subscriber.(create|findMany|update)
from to via pattern
src/app/api/subscribers/route.ts src/lib/services/subscriber-service.ts Route handlers call service functions createSubscriber|searchSubscribers
from to via pattern
prisma/schema.prisma prisma/schema.prisma Subscriber.servicePlanId references ServicePlan.id 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.

<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
  1. 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])
  2. 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.

  3. 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])
  4. Update TENANT_SCOPED_MODELS in prisma-tenant.ts to include "subscriber", "servicePlan", "tenantSettings". Add query extension blocks.

  5. 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).
  6. 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.
  7. 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.

  1. 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

<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>
After completion, create `.planning/phases/02-subscriber-and-billing-core/02-03-SUMMARY.md`