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>
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 |
|
true |
|
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])-
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])
-
Replace Subscriber.zone String? with Subscriber.zoneId String? (FK to Zone):
- Remove
zone String?field - Add
zoneId String?andzone Zone? @relation(fields: [zoneId], references: [id]) - Add @@index([tenantId, zoneId])
- Remove
-
Add reverse relations on Zone:
subscribers Subscriber[],assignments ZoneAssignment[]Add reverse relation on User:zoneAssignments ZoneAssignment[] -
Run
npx prisma migrate dev --name add-zonesto create migration. -
Add "Zone" to AppSubjects in types.ts (it is not currently listed). Add ZoneAssignment does NOT need its own subject — managed through Zone.
-
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
- ADMIN: already has
-
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 devsucceeds with no errorsnpx tsc --noEmitpasses (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.
-
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); } -
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 passnpx tsc --noEmitpasses 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.
<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>