Files
NetForge/.planning/phases/03-operational-modules/03-01-SUMMARY.md
kevin-asprec 2321bf2ada docs(03-01): complete zone management plan
Tasks completed: 2/2
- Task 1: Zone schema, migration, and tenant scoping
- Task 2: Zone service, API routes, and integration tests

SUMMARY: .planning/phases/03-operational-modules/03-01-SUMMARY.md

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

7.7 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established duration completed
03-operational-modules 01 database
prisma
zones
multi-tenancy
collector-routing
casl
api
phase provides
02-subscriber-and-billing-core Subscriber model with zone String? field that was replaced by zoneId FK
phase provides
01-foundation withTenantContext() Prisma extension, withPermission() HOF, CASL types/permissions
Zone model with tenant scoping and @@unique([tenantId, name])
ZoneAssignment join table (collector-to-zone mapping)
Subscriber.zoneId FK replacing Subscriber.zone String?
zone-service.ts with 10 exported functions (createZone, updateZone, listZones, getZone, assignSubscriberToZone, removeSubscriberFromZone, assignCollectorToZone, removeCollectorFromZone, getCollectorZones, getCollectorSubscribers)
5 API route files (zones CRUD, subscriber assignment, collector subscriber scoping)
25 integration tests covering all zone operations and cross-tenant isolation
03-02 (Collector Management) — zone assignment is foundation for collector routes
03-03 and later — any phase working with subscriber data must handle zoneId not zone
added patterns
Collector security boundary: getCollectorSubscribers throws if no zone assignments (zero-access default)
ZoneAssignment as join table with upsert for idempotent collector assignment
Zone tenant scoping follows full 15-operation extension block pattern
created modified
prisma/migrations/20260305000000_add_zones/migration.sql
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
prisma/schema.prisma
src/lib/prisma-tenant.ts
src/lib/casl/types.ts
src/lib/casl/permissions.ts
src/lib/services/subscriber-service.ts
src/app/api/subscribers/route.ts
src/app/api/subscribers/[id]/route.ts
src/lib/__tests__/subscriber.test.ts
Collector security boundary enforced at data layer: getCollectorSubscribers throws on no zone assignments (not just returns empty)
ZoneAssignment upsert for idempotent assign: duplicate assignment returns existing record, not error
Subscriber.zone String? replaced with Subscriber.zoneId FK: cleaner relational model, enables JOIN queries
COLLECTOR can('read', 'Zone') in permissions: coarse-grained, data layer enforces which zones
OFFICE_STAFF can('manage', 'Zone'): zone configuration is staff responsibility
Migration applied via psql exec (Docker container) + prisma migrate resolve --applied: non-interactive CLI workaround
Zone security pattern: THROW (not empty return) when collector has no zone assignments
Collector API self-restriction: collector can only query their own subscriber list; admin/staff can query any
15min 2026-03-05

Phase 3 Plan 01: Zone Management Summary

Zone and ZoneAssignment models with collector-scoped subscriber queries — security boundary enforced by throwing on unassigned collectors

Performance

  • Duration: 15 min
  • Started: 2026-03-04T23:20:14Z
  • Completed: 2026-03-05T07:31:00Z
  • Tasks: 2
  • Files modified: 15

Accomplishments

  • Zone model with tenant scoping, ZoneAssignment collector-to-zone join table, and Subscriber.zoneId FK replacing the old String? zone field
  • Complete zone CRUD API with withPermission HOF enforcement (5 routes)
  • Collector security boundary: getCollectorSubscribers throws if collector has no zone assignments, enforced at service layer
  • 25 integration tests — all passing — covering CRUD, subscriber assignment, collector scoping, and cross-tenant isolation

Task Commits

Each task was committed atomically:

  1. Task 1: Zone schema, migration, and tenant scoping - 56f5d07 (feat)
  2. Task 2: Zone service, API routes, and integration tests - 214df6c (feat)

Plan metadata: [pending] (docs: complete plan)

Files Created/Modified

  • prisma/schema.prisma - Zone and ZoneAssignment models, Subscriber.zoneId FK
  • prisma/migrations/20260305000000_add_zones/migration.sql - Migration SQL
  • src/lib/prisma-tenant.ts - Zone and ZoneAssignment tenant scoping blocks (full 15 ops each)
  • src/lib/casl/types.ts - Added "Zone" to AppSubjects
  • src/lib/casl/permissions.ts - OFFICE_STAFF: manage Zone, COLLECTOR: read Zone
  • src/lib/services/zone-service.ts - 10 service functions for zone management
  • src/app/api/zones/route.ts - GET list, POST create
  • src/app/api/zones/[id]/route.ts - GET single, PUT update
  • src/app/api/zones/[id]/subscribers/route.ts - POST assign, DELETE remove
  • src/app/api/collectors/[id]/subscribers/route.ts - GET collector-scoped subscriber list
  • src/lib/__tests__/zone-service.test.ts - 25 integration tests
  • src/lib/services/subscriber-service.ts - zone -> zoneId field update (auto-fix)
  • src/app/api/subscribers/route.ts - zone -> zoneId body field (auto-fix)
  • src/app/api/subscribers/[id]/route.ts - zone -> zoneId update field (auto-fix)
  • src/lib/__tests__/subscriber.test.ts - removed zone field from test (auto-fix)

Decisions Made

  • Collector security boundary enforced at data layer: getCollectorSubscribers throws (not empty return) when collector has no zone assignments — "no zones = no access" is explicit, not silent
  • ZoneAssignment upsert for idempotent collector assignment: duplicate assign calls don't throw errors
  • Subscriber.zone String? replaced with Subscriber.zoneId FK: required for proper relational queries and JOIN-based ordering in getCollectorSubscribers
  • COLLECTOR gets can("read", "Zone") in CASL: coarse-grained gate, data layer enforces which specific zones
  • Migration applied via Docker exec + prisma migrate resolve --applied: prisma migrate dev fails in non-interactive environments; this is the established workaround from Phase 1

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Fixed subscriber-service.ts zone String? -> zoneId FK

  • Found during: Task 2 (zone service creation)
  • Issue: subscriber-service.ts still referenced zone field (CreateSubscriberInput, UpdateSubscriberInput, create/update logic) — field no longer exists in schema after migration
  • Fix: Updated input interfaces to use zoneId?: string, updated create/update to use zoneId instead of zone?.trim(), fixed API routes that passed zone to the service
  • Files modified: src/lib/services/subscriber-service.ts, src/app/api/subscribers/route.ts, src/app/api/subscribers/[id]/route.ts, src/lib/tests/subscriber.test.ts
  • Verification: npx tsc --noEmit passes, subscriber test fixture updated
  • Committed in: 214df6c (Task 2 commit)

Total deviations: 1 auto-fixed (Rule 1 - Bug) Impact on plan: Necessary for correctness — the old zone String field was removed from schema, service code must use zoneId FK.

Issues Encountered

  • prisma migrate dev --name add-zones failed in non-interactive bash environment (pre-existing issue from Phase 1). Workaround: generate SQL via prisma migrate diff --script, write migration file manually, apply via Docker exec psql, then run prisma migrate resolve --applied. This is the same pattern used in Phase 1.

Next Phase Readiness

  • Zone foundation complete — ready for 03-02 (Collector Management) which builds on zone assignments
  • Subscriber.zoneId FK in place — collector routing is now data-layer enforced
  • getCollectorSubscribers security boundary tested and verified

Phase: 03-operational-modules Completed: 2026-03-05