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) — sequential - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.1 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 workflow — a collector can only collect from subscribers in their assigned zones. This must exist before collector field collection (03-02) can enforce proper scoping.
Output: Zone Prisma model, zone CRUD service and APIs, collector-to-zone and subscriber-to-zone assignment, scoped subscriber list for collectors, 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 @prisma/schema.prisma @src/lib/prisma-tenant.ts @src/lib/services/subscriber-service.ts @src/lib/middleware/with-permission.ts Task 1: Zone and ZoneAssignment Prisma models + migration prisma/schema.prisma, src/lib/prisma-tenant.ts Add two new models to Prisma schema:-
Zone model:
- id (uuid PK), tenantId, name (String), description (String?), isActive (Boolean default true)
- createdAt, updatedAt
- @@unique([tenantId, name]) — zone names unique per tenant
- @@index([tenantId])
- Relation: subscribers Subscriber[] (via Subscriber.zoneId — update Subscriber to add zoneId optional FK)
- Relation: assignments ZoneAssignment[]
-
ZoneAssignment model:
- id (uuid PK), tenantId, zoneId (FK to Zone), userId (FK to User — the collector)
- createdAt
- @@unique([tenantId, zoneId, userId]) — prevent duplicate assignments
- @@index([tenantId]), @@index([userId]), @@index([zoneId])
-
Update Subscriber model:
- The Subscriber already has
zone String?field. Replace it with a proper FK: - Add
zoneId String?andzone Zone? @relation(fields: [zoneId], references: [id]) - Remove the old
zone String?field (it was a placeholder for Phase 3) - Add @@index([tenantId, zoneId])
- The Subscriber already has
-
Update User model:
- Add relation:
zoneAssignments ZoneAssignment[]
- Add relation:
-
Add "zone" and "zoneAssignment" to TENANT_SCOPED_MODELS in
src/lib/prisma-tenant.tsand extend the withTenantContext() $extends block following the existing pattern. -
Run
npx prisma migrate dev --name add-zonesto create the migration.
Important: The old zone String? on Subscriber is being replaced with zoneId String? (FK). The migration needs to handle this — drop the old column, add new column. No data migration needed (no production data).
- npx prisma migrate dev completes without errors
- npx prisma generate succeeds
- Schema has Zone, ZoneAssignment models
- Subscriber has zoneId FK instead of zone String
Zone and ZoneAssignment models exist in schema, Subscriber.zoneId replaces zone String, TENANT_SCOPED_MODELS updated, migration applied.
Follow existing service patterns: take tenantPrisma client as first arg (same as PaymentService, SubscriberService). Use as any cast pattern for tenantId injection (documented in 02-03 decision).
API Routes:
GET /api/zones— list zones (ADMIN, OFFICE_STAFF, COLLECTOR can read)POST /api/zones— create zone (ADMIN only)GET /api/zones/[id]— get zone detail with subscriber countPUT /api/zones/[id]— update zone (ADMIN only)POST /api/zones/[id]/subscribers— assign subscriber to zone, body: { subscriberId }. ADMIN, OFFICE_STAFF.DELETE /api/zones/[id]/subscribers— remove subscriber from zone, body: { subscriberId }. ADMIN, OFFICE_STAFF.GET /api/collectors/[id]/subscribers— get subscribers for a specific collector (scoped by zone assignments). ADMIN, OFFICE_STAFF can query any collector; COLLECTOR can only query self.
Use withPermission() HOF pattern from existing API routes. For dynamic [id] routes, use the closure pattern documented in 02-01 decision (withPermission doesn't support dynamic params directly).
Integration Tests (src/lib/__tests__/zone-service.test.ts):
- Zone CRUD (create, update, list, deactivate)
- Zone name uniqueness within tenant
- Subscriber zone assignment and removal
- Collector zone assignment and removal
- getCollectorSubscribers returns only subscribers in collector's zones
- getCollectorSubscribers returns empty for collector with no zone assignments
- Collector cannot be assigned to zone if they don't have COLLECTOR role
- Cross-tenant isolation (zone from tenant A not visible to tenant B)
Follow existing test patterns: beforeAll creates tenant+user+accounts, afterAll cleans up in correct order. Add Zone and ZoneAssignment to cleanup order.
- npx vitest run src/lib/__tests__/zone-service.test.ts — all tests pass
- npx vitest run — full suite passes (no regressions)
ZoneService with zone CRUD, subscriber/collector assignment, and collector-scoped subscriber queries all working. API routes enforce RBAC. Integration tests prove zone scoping and tenant isolation.
<success_criteria>
- Zone and ZoneAssignment models in Prisma schema with migration applied
- ZoneService handles zone CRUD, subscriber assignment, collector assignment, and scoped queries
- API routes enforce RBAC (admin creates zones, collectors query their subscribers)
- Integration tests prove collector can only see subscribers in their assigned zones
- Full test suite passes with no regressions </success_criteria>