Files
NetForge/.planning/phases/03-operational-modules/03-01-PLAN.md
kevin-asprec ef0150654b 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) — sequential
- Ready for execution

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

188 lines
9.1 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<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>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: Zone and ZoneAssignment Prisma models + migration</name>
<files>prisma/schema.prisma, src/lib/prisma-tenant.ts</files>
<action>
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).
</action>
<verify>
- `npx prisma migrate dev` completes without errors
- `npx prisma generate` succeeds
- Schema has Zone, ZoneAssignment models
- Subscriber has zoneId FK instead of zone String
</verify>
<done>Zone and ZoneAssignment models exist in schema, Subscriber.zoneId replaces zone String, TENANT_SCOPED_MODELS updated, migration applied.</done>
</task>
<task type="auto">
<name>Task 2: ZoneService + API routes + integration tests</name>
<files>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</files>
<action>
**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.
</action>
<verify>
- `npx vitest run src/lib/__tests__/zone-service.test.ts` — all tests pass
- `npx vitest run` — full suite passes (no regressions)
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- 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)
</verification>
<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>
<output>
After completion, create `.planning/phases/03-operational-modules/03-01-SUMMARY.md`
</output>