--- phase: 03-operational-modules plan: "01" type: execute wave: 1 depends_on: [] files_modified: - prisma/schema.prisma - src/lib/prisma-tenant.ts - 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 autonomous: true must_haves: truths: - "Admin can create, update, and deactivate zones for their tenant" - "Admin can assign subscribers to a zone" - "Admin can assign a collector user to a zone" - "A collector can only see subscribers assigned to their zone(s)" artifacts: - path: "prisma/schema.prisma" provides: "Zone model with name, description, isActive; ZoneAssignment linking collector users to zones" contains: "model Zone" - path: "src/lib/services/zone-service.ts" provides: "Zone CRUD, subscriber zone assignment, collector zone assignment, getCollectorSubscribers" exports: ["ZoneService"] - path: "src/app/api/zones/route.ts" provides: "GET list zones, POST create zone" exports: ["GET", "POST"] - path: "src/lib/__tests__/zone-service.test.ts" provides: "Integration tests for zone CRUD, assignment, collector scoping" min_lines: 80 key_links: - from: "src/lib/services/zone-service.ts" to: "prisma/schema.prisma" via: "Prisma client queries on Zone and ZoneAssignment" pattern: "prisma\\.zone\\." - from: "src/app/api/collectors/[id]/subscribers/route.ts" to: "src/lib/services/zone-service.ts" via: "getCollectorSubscribers returns only zone-scoped subscribers" pattern: "getCollectorSubscribers" --- Create the zone/territory system that scopes collectors to specific subscriber groups. 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. @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/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: 1. **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[] 2. **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]) 3. **Update Subscriber model:** - The Subscriber already has `zone String?` field. Replace it with a proper FK: - Add `zoneId String?` and `zone Zone? @relation(fields: [zoneId], references: [id])` - Remove the old `zone String?` field (it was a placeholder for Phase 3) - Add @@index([tenantId, zoneId]) 4. **Update User model:** - Add relation: `zoneAssignments ZoneAssignment[]` 5. **Add "zone" and "zoneAssignment" to TENANT_SCOPED_MODELS** in `src/lib/prisma-tenant.ts` and extend the withTenantContext() $extends block following the existing pattern. 6. Run `npx prisma migrate dev --name add-zones` to 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. Task 2: ZoneService + API routes + 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 **ZoneService** (`src/lib/services/zone-service.ts`): - `createZone(db, { name, description })` — creates zone, returns zone - `updateZone(db, zoneId, { name?, description?, isActive? })` — updates zone - `listZones(db)` — returns all zones for tenant (active and inactive) - `assignSubscriberToZone(db, subscriberId, zoneId)` — updates subscriber.zoneId - `removeSubscriberFromZone(db, subscriberId)` — sets subscriber.zoneId to null - `assignCollectorToZone(db, userId, zoneId)` — creates ZoneAssignment (validates user has COLLECTOR role) - `removeCollectorFromZone(db, userId, zoneId)` — deletes ZoneAssignment - `getCollectorZones(db, userId)` — returns zones assigned to a collector - `getCollectorSubscribers(db, userId)` — returns subscribers in all zones assigned to this collector (the key scoping query). Include subscriber status and outstanding invoice count for the collector's field view. - `getZoneSubscribers(db, zoneId)` — returns subscribers in a specific zone 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 count - `PUT /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. - Zone CRUD: create, update, deactivate zones - Subscriber assignment: assign/remove subscriber to/from zone - Collector assignment: assign/remove collector to/from zone - Collector scoping: collector sees only their zone's subscribers - Tenant isolation: zones are tenant-scoped - All existing tests still pass (no regressions from Subscriber.zone -> zoneId migration) - 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 After completion, create `.planning/phases/03-operational-modules/03-01-SUMMARY.md`