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