Files
kevin-asprec d54b517e2e docs(03): create phase plan
Phase 03: Operational Modules
- 5 plans in 3 waves
- Wave 1: 03-01 (zones), 03-03 (tickets) — parallel
- Wave 2: 03-02 (collector collections), 03-04 (job orders) — parallel
- Wave 3: 03-05 (technician compensation)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:11:44 +08:00

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
03-operational-modules 01 execute 1
prisma/schema.prisma
src/lib/prisma-tenant.ts
src/lib/services/zone-service.ts
src/lib/casl/types.ts
src/lib/casl/permissions.ts
src/app/api/zones/route.ts
src/app/api/zones/[id]/route.ts
src/app/api/zones/[id]/subscribers/route.ts
src/app/api/collectors/[id]/subscribers/route.ts
src/lib/__tests__/zone-service.test.ts
true
truths artifacts key_links
Admin can create, read, update zones with name and description
Admin can assign subscribers to zones via zoneId FK
Admin can assign collectors to zones via ZoneAssignment join
Collector can only query subscribers within their assigned zones
Zone data is tenant-scoped — Tenant B cannot see Tenant A zones
path provides contains
prisma/schema.prisma Zone and ZoneAssignment models, Subscriber.zoneId FK replacing zone String? model Zone
path provides exports
src/lib/services/zone-service.ts Zone CRUD, subscriber assignment, collector zone scoping
createZone
updateZone
listZones
assignSubscriberToZone
getCollectorSubscribers
path provides contains
src/lib/prisma-tenant.ts Tenant-scoped query blocks for Zone and ZoneAssignment zone
path provides min_lines
src/lib/__tests__/zone-service.test.ts Integration tests for zone CRUD, assignment, collector scoping 100
from to via pattern
src/lib/services/zone-service.ts prisma/schema.prisma tenantPrisma.zone and tenantPrisma.zoneAssignment queries tenantPrisma.zone.
from to via pattern
src/app/api/zones/route.ts src/lib/services/zone-service.ts withPermission HOF wrapping service calls withPermission.*Zone
from to via pattern
src/app/api/collectors/[id]/subscribers/route.ts src/lib/services/zone-service.ts getCollectorSubscribers for zone-scoped subscriber list getCollectorSubscribers
Create the zone management system: Zone model, ZoneAssignment model (collector-to-zone), replace Subscriber.zone String? with Subscriber.zoneId FK, zone CRUD API, subscriber-to-zone assignment, and collector-scoped subscriber queries.

Purpose: Zones are the foundation for collector routing — collectors can only collect from subscribers in their assigned zones. This is a security boundary enforced at the data layer. Output: Zone/ZoneAssignment models, zone-service.ts, 5 API routes, integration 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/03-operational-modules/03-CONTEXT.md @.planning/phases/03-operational-modules/03-RESEARCH.md @prisma/schema.prisma @src/lib/prisma-tenant.ts @src/lib/casl/types.ts @src/lib/casl/permissions.ts @src/lib/services/payment-service.ts (pattern reference for tenant-scoped service functions) @src/lib/__tests__/payment.test.ts (pattern reference for integration test setup/cleanup) Task 1: Zone schema, migration, and tenant scoping prisma/schema.prisma src/lib/prisma-tenant.ts src/lib/casl/types.ts src/lib/casl/permissions.ts 1. Add Zone model to schema.prisma: - id (uuid), tenantId, name (String), description (String?), isActive (Boolean default true), createdAt, updatedAt - @@unique([tenantId, name]), @@index([tenantId])
  1. Add ZoneAssignment model (collector-to-zone join):

    • id (uuid), tenantId, userId (String — the collector user), zoneId (String — FK to Zone)
    • Relations: user -> User, zone -> Zone
    • @@unique([tenantId, userId, zoneId]), @@index([tenantId]), @@index([userId]), @@index([zoneId])
  2. Replace Subscriber.zone String? with Subscriber.zoneId String? (FK to Zone):

    • Remove zone String? field
    • Add zoneId String? and zone Zone? @relation(fields: [zoneId], references: [id])
    • Add @@index([tenantId, zoneId])
  3. Add reverse relations on Zone: subscribers Subscriber[], assignments ZoneAssignment[] Add reverse relation on User: zoneAssignments ZoneAssignment[]

  4. Run npx prisma migrate dev --name add-zones to create migration.

  5. Add "Zone" to AppSubjects in types.ts (it is not currently listed). Add ZoneAssignment does NOT need its own subject — managed through Zone.

  6. Update permissions.ts:

    • ADMIN: already has manage all
    • OFFICE_STAFF: add can("manage", "Zone")
    • COLLECTOR: add can("read", "Zone") (can see zones they are assigned to)
    • TECHNICIAN/CLIENT: no zone access
  7. Add Zone and ZoneAssignment to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension blocks (copy from subscriber block pattern — findMany, findFirst, findFirstOrThrow, findUnique, findUniqueOrThrow, create, createMany, update, updateMany, delete, deleteMany, upsert, count, aggregate, groupBy). Missing any operation is a security hole.

    • npx prisma migrate dev succeeds with no errors
    • npx tsc --noEmit passes (no TypeScript errors)
    • Grep prisma-tenant.ts confirms both "zone" and "zoneAssignment" appear in TENANT_SCOPED_MODELS
    • Grep types.ts confirms "Zone" in AppSubjects Zone and ZoneAssignment models exist in schema, migration applied, tenant scoping configured, CASL subjects and permissions updated.
Task 2: Zone service, API routes, and integration tests src/lib/services/zone-service.ts src/app/api/zones/route.ts src/app/api/zones/[id]/route.ts src/app/api/zones/[id]/subscribers/route.ts src/app/api/collectors/[id]/subscribers/route.ts src/lib/__tests__/zone-service.test.ts 1. Create src/lib/services/zone-service.ts with pure functions (tenantPrisma as first arg, tenantId as second for transactions): - `createZone(tenantPrisma, tenantId, { name, description })` — create zone, return zone - `updateZone(tenantPrisma, zoneId, { name?, description?, isActive? })` — update zone - `listZones(tenantPrisma)` — return all zones with subscriber count and assigned collector count - `getZone(tenantPrisma, zoneId)` — single zone with relations - `assignSubscriberToZone(tenantPrisma, subscriberId, zoneId)` — update subscriber.zoneId - `removeSubscriberFromZone(tenantPrisma, subscriberId)` — set subscriber.zoneId to null - `assignCollectorToZone(tenantPrisma, tenantId, userId, zoneId)` — create ZoneAssignment (validate user has COLLECTOR role) - `removeCollectorFromZone(tenantPrisma, tenantId, userId, zoneId)` — delete ZoneAssignment - `getCollectorSubscribers(tenantPrisma, tenantId, collectorUserId)` — find all zones assigned to collector, then find all subscribers in those zones. THROW error if collector has no zone assignments (security boundary per RESEARCH.md). Return subscribers with basic info (id, accountNumber, firstName, lastName, address, zone name). - `getCollectorZones(tenantPrisma, collectorUserId)` — return zones assigned to a collector
  1. Create API routes following withPermission pattern:

    • GET /api/zones: withPermission("read", "Zone") -> listZones
    • POST /api/zones: withPermission("create", "Zone") -> createZone, validate name required
    • GET /api/zones/[id]: withPermission("read", "Zone") -> getZone
    • PUT /api/zones/[id]: withPermission("update", "Zone") -> updateZone
    • POST /api/zones/[id]/subscribers: withPermission("update", "Zone") -> assignSubscriberToZone (body: { subscriberId })
    • DELETE /api/zones/[id]/subscribers: withPermission("update", "Zone") -> removeSubscriberFromZone (body: { subscriberId })
    • GET /api/collectors/[id]/subscribers: withPermission("read", "Subscriber") -> getCollectorSubscribers (collector can only query their own; admin/staff can query any collector)

    Use the dynamic route handler pattern from 02-04: export async function GET(req, { params }) { return withPermission(...)(async (req, { user }) => { const { id } = params; ... })(req); }

  2. Create src/lib/tests/zone-service.test.ts integration tests:

    • Setup: create tenant, admin user, collector user, seed COA, create service plan, create 3 subscribers, create 2 zones
    • Test createZone: creates zone with name/description
    • Test createZone duplicate name: throws on duplicate name within tenant
    • Test updateZone: updates name, description, isActive
    • Test listZones: returns zones with counts
    • Test assignSubscriberToZone: subscriber.zoneId updated
    • Test assignCollectorToZone: ZoneAssignment created, validates COLLECTOR role
    • Test getCollectorSubscribers: returns only subscribers in collector's assigned zones
    • Test getCollectorSubscribers with no zones: throws error
    • Test cross-tenant isolation: zone created in Tenant A is invisible to Tenant B query
    • Cleanup afterAll in reverse order: zoneAssignments -> subscribers -> servicePlans -> zones -> users -> tenant (extend the established cleanup pattern)

    Follow the exact test setup pattern from payment.test.ts — use TS = Date.now() suffix, create via raw prisma for setup, test via tenantPrisma.

    • npx vitest run src/lib/__tests__/zone-service.test.ts — all tests pass
    • npx tsc --noEmit passes Zone CRUD works, subscribers can be assigned to zones, collectors can be assigned to zones, collector-scoped subscriber queries enforce zone boundary, cross-tenant isolation verified. All tests pass.
- `npx prisma migrate dev` succeeds (schema valid) - `npx tsc --noEmit` passes (no TypeScript errors) - `npx vitest run src/lib/__tests__/zone-service.test.ts` — all tests green - Zone CRUD, subscriber assignment, collector scoping, and tenant isolation verified

<success_criteria>

  • Zone and ZoneAssignment models exist with proper tenant scoping
  • Subscriber.zone String? replaced with Subscriber.zoneId FK
  • Zone CRUD API routes work with withPermission enforcement
  • Collectors can only query subscribers in their assigned zones
  • Cross-tenant isolation proven by test
  • All integration tests pass </success_criteria>
After completion, create `.planning/phases/03-operational-modules/03-01-SUMMARY.md`