diff --git a/.planning/STATE.md b/.planning/STATE.md index 1521a73..3238f9b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -9,12 +9,12 @@ See: .planning/PROJECT.md (updated 2026-03-04) ## Current Position -Phase: 2 of 5 (Subscriber and Billing Core) — COMPLETE -Plan: 5 of 5 in phase 2 (11/20 total complete) -Status: Phase 2 complete. 02-01 (COA), 02-02 (JournalEntryService), 02-03 (Subscriber), 02-04 (BillingEngine), 02-05 (PaymentService) all done. -Last activity: 2026-03-04 — Completed 02-05-PLAN.md (PaymentService, 29 tests, 265 total) +Phase: 3 of 5 (Operational Modules) — In progress +Plan: 1 of 5 in phase 3 (12/20 total complete) +Status: Phase 3 started. 03-01 (Zone Management) complete. Zones, ZoneAssignment, collector scoping all implemented. +Last activity: 2026-03-05 — Completed 03-01-PLAN.md (Zone management, 25 tests) -Progress: [███████████░] 55% (11/20 plans across all phases) +Progress: [████████████░] 60% (12/20 plans across all phases) ## Performance Metrics @@ -31,8 +31,8 @@ Progress: [███████████░] 55% (11/20 plans across all pha | 02-subscriber-and-billing-core | 5/5 complete | 57 min | 11.4 min | **Recent Trend:** -- Last 9 plans: 01-02 (8 min), 01-03 (9 min), 01-04 (7 min), 01-05 (6 min), 02-01 (7 min), 02-03 (7 min), 02-02 (16 min), 02-04 (12 min), 02-05 (8 min) -- Trend: stable — 02-05 was complex but tests passed on second run +- Last 10 plans: 01-02 (8 min), 01-03 (9 min), 01-04 (7 min), 01-05 (6 min), 02-01 (7 min), 02-03 (7 min), 02-02 (16 min), 02-04 (12 min), 02-05 (8 min), 03-01 (15 min) +- Trend: stable — 03-01 had a migration workaround but tests passed on first run *Updated after each plan completion* @@ -95,6 +95,11 @@ Recent decisions affecting current work: - [02-05]: Overpayment to subscriber.creditBalance atomically with JE — same pattern as invoice.amountPaid from 02-04 - [02-05]: Outstanding report computed in JS after fetching — Prisma doesn't support computed fields in WHERE/ORDER BY; acceptable for ISP scale - [02-05]: Test invoiceCounter for periodStart uniqueness — monotonic counter generates unique periodStart per invoice, avoids @@unique([tenantId, subscriberId, periodStart]) in test helpers +- [03-01]: Collector security boundary enforced at service layer: getCollectorSubscribers THROWS (not empty return) when collector has no zone assignments — zero-access default +- [03-01]: ZoneAssignment upsert for idempotent collector assignment — duplicate assign calls don't throw errors +- [03-01]: Subscriber.zone String? replaced with Subscriber.zoneId FK — required for relational queries and JOIN-based ordering +- [03-01]: COLLECTOR gets can("read", "Zone") in CASL: coarse-grained gate, data layer enforces which specific zones +- [03-01]: Migration applied via Docker exec psql + prisma migrate resolve --applied (non-interactive CLI workaround) ### Pending Todos @@ -112,6 +117,6 @@ None. ## Session Continuity -Last session: 2026-03-04T15:52:55Z -Stopped at: Completed 02-05-PLAN.md (PaymentService + 29 tests — Phase 2 complete) +Last session: 2026-03-05T07:31:00Z +Stopped at: Completed 03-01-PLAN.md (Zone management — 25 tests, Zone/ZoneAssignment models, collector scoping) Resume file: None diff --git a/.planning/phases/03-operational-modules/03-01-SUMMARY.md b/.planning/phases/03-operational-modules/03-01-SUMMARY.md new file mode 100644 index 0000000..c50ba9c --- /dev/null +++ b/.planning/phases/03-operational-modules/03-01-SUMMARY.md @@ -0,0 +1,148 @@ +--- +phase: 03-operational-modules +plan: "01" +subsystem: database +tags: [prisma, zones, multi-tenancy, collector-routing, casl, api] + +# Dependency graph +requires: + - phase: 02-subscriber-and-billing-core + provides: Subscriber model with zone String? field that was replaced by zoneId FK + - phase: 01-foundation + provides: withTenantContext() Prisma extension, withPermission() HOF, CASL types/permissions + +provides: + - 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 + +affects: + - 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 + +# Tech tracking +tech-stack: + 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" + +key-files: + created: + - 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 + modified: + - 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 + +key-decisions: + - "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" + +patterns-established: + - "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" + +# Metrics +duration: 15min +completed: 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*