docs(05): create gap closure plans for milestone audit findings

Phase 05: 2 gap closure plans in 1 wave
- 05-06: P0 tenant scoping fix (6 models) + CASL subject correction
- 05-07: E2E test coverage gaps + Phase 2 verification correction
- Both plans are parallel (Wave 1, no dependencies)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kevin-asprec
2026-03-05 18:15:44 +08:00
parent e70501e8bc
commit 9fdca2ccf7
3 changed files with 348 additions and 9 deletions

View File

@@ -0,0 +1,166 @@
---
phase: 05-visibility-and-client-portal
plan: 06
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/prisma-tenant.ts
- src/lib/casl/types.ts
- src/lib/casl/permissions.ts
- src/app/api/collections/route.ts
- src/app/api/collections/[id]/route.ts
- src/app/api/collections/[id]/void/route.ts
- src/app/api/remittances/route.ts
- src/app/api/remittances/[id]/verify/route.ts
- src/app/api/reports/collections/route.ts
autonomous: true
gap_closure: true
must_haves:
truths:
- "All 6 missing models (expense, expenseCategory, inventoryItem, stockMovement, vendor, ticketComment) are registered in TENANT_SCOPED_MODELS and have $extends query blocks"
- "Collection and remittance API routes use dedicated CASL subjects instead of borrowing Subscriber"
artifacts:
- path: "src/lib/prisma-tenant.ts"
provides: "Tenant scoping for all 28 tenant-scoped models"
contains: "expense.*expenseCategory.*inventoryItem.*stockMovement.*vendor.*ticketComment"
- path: "src/lib/casl/types.ts"
provides: "Collection and Remittance subjects in AppSubjects"
contains: "Collection.*Remittance"
- path: "src/lib/casl/permissions.ts"
provides: "Collection and Remittance permission rules per role"
contains: "Collection.*Remittance"
key_links:
- from: "src/lib/prisma-tenant.ts"
to: "withTenantContext query extensions"
via: "TENANT_SCOPED_MODELS array + $extends blocks"
pattern: "expense.*expenseCategory.*inventoryItem.*stockMovement.*vendor.*ticketComment"
- from: "src/app/api/collections/route.ts"
to: "src/lib/casl/permissions.ts"
via: "withPermission HOF"
pattern: "withPermission.*Collection"
---
<objective>
Fix the P0 tenant isolation security gap and correct CASL subject naming for collection/remittance routes.
Purpose: Close the critical multi-tenancy vulnerability where 6 models bypass application-layer tenant scoping, and improve permission precision by giving collection/remittance routes their own CASL subjects.
Output: Updated prisma-tenant.ts with all 6 missing models, updated CASL types and permissions, updated collection/remittance route handlers.
</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/v1-MILESTONE-AUDIT.md
@src/lib/prisma-tenant.ts
@src/lib/casl/types.ts
@src/lib/casl/permissions.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Add 6 missing models to tenant scoping</name>
<files>src/lib/prisma-tenant.ts</files>
<action>
1. Add the 6 missing model names to the TENANT_SCOPED_MODELS array on line 33:
- "expense"
- "expenseCategory"
- "inventoryItem"
- "stockMovement"
- "vendor"
- "ticketComment"
2. Add $extends query blocks for each of the 6 models inside withTenantContext(), following the EXACT same pattern used by existing models (e.g., the `ticket` or `collection` blocks). Each model needs these query methods:
- findMany: inject tenantId into args.where
- findFirst: inject tenantId into args.where
- findFirstOrThrow: inject tenantId into args.where
- findUnique: route through findFirst with tenant guard (same pattern as existing models — check if "id" in args.where without tenantId, then use prisma.{model}.findFirst with tenantId)
- findUniqueOrThrow: same findFirst routing with throw on null
- create: strip nested tenant relation, inject tenantId into args.data
- createMany: handle array and single data, strip tenant, inject tenantId
- update: inject tenantId into args.where
- updateMany: inject tenantId into args.where
- delete: inject tenantId into args.where
- deleteMany: inject tenantId into args.where
- upsert: inject tenantId into args.where, strip tenant from args.create, inject tenantId
- count: inject tenantId into args.where
- aggregate: inject tenantId into args.where
Place the 6 new model blocks AFTER the existing `jobTypeRate` block (last current model at line ~1804) and BEFORE the closing of the $extends object.
IMPORTANT: Use the exact same eslint-disable comments as existing blocks for the tenant destructuring pattern. Copy the pattern from an existing block like `ticket` or `collection` verbatim — do NOT invent a new pattern.
</action>
<verify>
1. Run: grep -c "expense\|expenseCategory\|inventoryItem\|stockMovement\|vendor\|ticketComment" src/lib/prisma-tenant.ts — should show multiple matches per model
2. Run: node -e "const t = require('./src/lib/prisma-tenant'); console.log(t.TENANT_SCOPED_MODELS)" — verify all 28 models listed (or use TypeScript compilation check)
3. Run: npx tsc --noEmit — no type errors
4. Run: npx jest --testPathPattern="tenant|rbac|e2e" --passWithNoTests — existing tests still pass
</verify>
<done>TENANT_SCOPED_MODELS contains all 28 models. Each of the 6 new models has a complete $extends query block matching the established pattern. TypeScript compiles. Existing tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Add Collection and Remittance CASL subjects and update routes</name>
<files>src/lib/casl/types.ts, src/lib/casl/permissions.ts, src/app/api/collections/route.ts, src/app/api/collections/[id]/route.ts, src/app/api/collections/[id]/void/route.ts, src/app/api/remittances/route.ts, src/app/api/remittances/[id]/verify/route.ts, src/app/api/reports/collections/route.ts</files>
<action>
1. In src/lib/casl/types.ts, add "Collection" and "Remittance" to the AppSubjects union type (after "Payment", before "Zone").
2. In src/lib/casl/permissions.ts, update permission rules:
- OFFICE_STAFF: Add `can("manage", "Collection")` and `can("manage", "Remittance")`
- COLLECTOR: Add `can("create", "Collection")`, `can("read", "Collection")`, `can("create", "Remittance")`, `can("read", "Remittance")`
- Do NOT change ADMIN (already has `can("manage", "all")`)
- Do NOT change TECHNICIAN or CLIENT (they should not access collections/remittances)
3. Update collection API routes to use "Collection" subject instead of "Subscriber":
- src/app/api/collections/route.ts: POST uses withPermission("create", "Collection"), GET uses withPermission("read", "Collection")
- src/app/api/collections/[id]/route.ts: GET uses withPermission("read", "Collection")
- src/app/api/collections/[id]/void/route.ts: uses withPermission("update", "Collection")
4. Update remittance API routes to use "Remittance" subject instead of "Subscriber":
- src/app/api/remittances/route.ts: POST uses withPermission("create", "Remittance"), GET uses withPermission("read", "Remittance")
- src/app/api/remittances/[id]/verify/route.ts: uses withPermission("update", "Remittance")
5. Update collection report route:
- src/app/api/reports/collections/route.ts: GET uses withPermission("read", "Collection") instead of withPermission("read", "Subscriber")
6. Update JSDoc comments in each route file to reflect the new subject name.
IMPORTANT: The RBAC integration tests in api-rbac.test.ts mock withPermission. After changing subjects, verify the RBAC tests still pass. If tests mock specific subjects, update them to match the new subjects.
</action>
<verify>
1. Run: grep -rn "withPermission.*Subscriber" src/app/api/collections/ src/app/api/remittances/ src/app/api/reports/collections/ — should return ZERO matches
2. Run: grep -rn "withPermission.*Collection\|withPermission.*Remittance" src/app/api/ — should show all collection/remittance routes using correct subjects
3. Run: npx tsc --noEmit — no type errors
4. Run: npx jest --testPathPattern="rbac|e2e" — all tests pass
</verify>
<done>Collection and Remittance are proper CASL subjects. All collection/remittance routes use their dedicated subjects. RBAC tests pass. No route still uses "Subscriber" for collection/remittance operations.</done>
</task>
</tasks>
<verification>
1. TypeScript compilation passes: npx tsc --noEmit
2. All existing tests pass: npx jest
3. TENANT_SCOPED_MODELS has 28 entries (22 existing + 6 new)
4. Zero collection/remittance routes use "Subscriber" as CASL subject
5. grep for all 6 model names in prisma-tenant.ts $extends block confirms they exist
</verification>
<success_criteria>
- The P0 tenant isolation gap is closed: all tenant-scoped Prisma models have automatic tenantId injection
- Collection and remittance routes use semantically correct CASL subjects
- All existing tests continue to pass
- TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-06-SUMMARY.md`
</output>

View File

@@ -0,0 +1,171 @@
---
phase: 05-visibility-and-client-portal
plan: 07
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/__tests__/integration/e2e-workflows.test.ts
- .planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md
autonomous: true
gap_closure: true
must_haves:
truths:
- "E2E tests exercise the inventory receiving and expense recording workflows end-to-end with balanced journal entries"
- "E2E tests exercise the portal ticket creation flow from subscriber to staff ticket queue"
- "Phase 2 VERIFICATION.md reflects the actual fixed state (passed, 5/5)"
artifacts:
- path: "src/lib/__tests__/integration/e2e-workflows.test.ts"
provides: "E2E tests for inventory/expense and portal ticket workflows"
contains: "Inventory.*Expense|Portal.*Ticket"
- path: ".planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md"
provides: "Corrected verification status"
contains: "status: passed"
key_links:
- from: "e2e-workflows.test.ts"
to: "inventory-service.ts"
via: "service import and call"
pattern: "receiveStock|recordMovement"
- from: "e2e-workflows.test.ts"
to: "expense-service.ts"
via: "service import and call"
pattern: "createExpense|recordExpense"
- from: "e2e-workflows.test.ts"
to: "portal-ticket-service.ts"
via: "service import and call"
pattern: "createPortalTicket"
---
<objective>
Add E2E test coverage for Phase 4 inventory/expense workflows and Phase 5 portal ticket flow, and correct the Phase 2 VERIFICATION.md status.
Purpose: Close the E2E test coverage gaps identified in the milestone audit and fix the outdated verification document that still shows a gap that was resolved.
Output: Two new E2E workflow test suites and a corrected Phase 2 VERIFICATION.md.
</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/v1-MILESTONE-AUDIT.md
@src/lib/__tests__/integration/e2e-workflows.test.ts
@src/lib/services/inventory-service.ts
@src/lib/services/asset-service.ts
@src/lib/services/expense-service.ts
@src/lib/services/portal-ticket-service.ts
@.planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add inventory/expense and portal ticket E2E workflows</name>
<files>src/lib/__tests__/integration/e2e-workflows.test.ts</files>
<action>
Add two new describe blocks to the existing e2e-workflows.test.ts file. Follow the exact patterns established by the existing 3 workflows (same test helpers, same tenant setup, same cleanup approach, same trial balance verification pattern).
**Workflow 4: Inventory Receiving -> Expense Recording -> Trial Balance**
Add a describe block "Workflow 4: Inventory Receiving and Expense Recording" with these tests:
1. "receives inventory items and creates balanced journal entries"
- Import and call inventory-service to receive stock (e.g., receiveStock or the appropriate function that creates an INBOUND/RECEIVED stock movement)
- Verify the stock movement was created
- Verify a journal entry was posted (DR Inventory asset account / CR Cash or AP)
- Verify the JE is balanced (debits === credits)
2. "records an expense with automatic journal entry"
- Import and call expense-service to create an expense (with category and vendor)
- Need to create an expense category and vendor first (use the appropriate service functions or direct prisma calls matching existing test patterns)
- Verify the expense was created
- Verify a journal entry was posted (DR Expense account / CR Cash or AP)
- Verify the JE is balanced
3. "trial balance remains balanced after inventory and expense transactions"
- Use JournalEntryService.getTrialBalance (same pattern as existing workflows)
- Assert total debits === total credits > 0
**Workflow 5: Portal Ticket Creation -> Staff Queue**
Add a describe block "Workflow 5: Portal Ticket Submission to Staff Queue" with these tests:
1. "subscriber creates portal ticket and it appears in staff ticket queue"
- Create a subscriber (use existing createSubscriber helper or the pattern from existing tests)
- Import and call portal-ticket-service's createPortalTicket (or equivalent function) to create a ticket as the subscriber
- This should trigger ensurePortalUser internally (creating a shadow User with CLIENT role)
- Verify the ticket was created with source=SUBSCRIBER
- Query tickets via ticket-service's list/search function (staff perspective) and verify the portal ticket appears in the results
2. "portal ticket has correct subscriber association"
- Verify the ticket's createdById links to the shadow portal user
- Verify the shadow user exists with role CLIENT
IMPORTANT:
- Reuse the existing test's tenant, users, and cleanup infrastructure. The existing beforeAll creates a tenant and users — extend it to also create any additional data needed (expense categories, vendors, inventory items).
- Add cleanup for new records in the existing afterAll block (expense categories, expenses, vendors, inventory items, stock movements, plus any portal-created users and tickets).
- Follow the existing import pattern at the top of the file.
- Check the actual function signatures in inventory-service.ts, expense-service.ts, and portal-ticket-service.ts before writing calls — use the exact parameter shapes those functions expect.
</action>
<verify>
1. Run: npx jest --testPathPattern="e2e-workflows" --verbose — all workflows pass (existing 3 + new 2)
2. Run: npx jest --testPathPattern="e2e-workflows" --verbose 2>&1 | grep -c "PASS\|FAIL" — should show PASS
3. Verify new test count: npx jest --testPathPattern="e2e-workflows" --verbose 2>&1 | grep "Tests:" — should be more than the previous 16
</verify>
<done>E2E test file has 5 workflow suites. Inventory receiving and expense recording produce balanced JEs. Portal ticket appears in staff queue. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Correct Phase 2 VERIFICATION.md status</name>
<files>.planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md</files>
<action>
Update the Phase 2 VERIFICATION.md to reflect the actual current state:
1. In the YAML frontmatter:
- Change `status: gaps_found` to `status: passed`
- Change `score: 4/5 must-haves verified` to `score: 5/5 must-haves verified`
- Remove or update the `gaps:` section — change the truth #3 status from `failed` to `fixed` and add a note that the fix was applied in billing-service.ts (status: InvoiceStatus.SENT, issuedAt: new Date()) and validated by Phase 5 E2E tests
2. In the body:
- Update Truth #3 status from "FAILED" to "VERIFIED (fixed post-verification)" in the Observable Truths table
- Add a brief note in the Evidence column: "Fixed: billing-service.ts now sets status=SENT and issuedAt on invoice creation. Validated by Phase 5 E2E billing workflow test."
- Update the Key Link "billing-service.ts -> Invoice (status=SENT)" from "NOT WIRED" to "WIRED" with note "Fixed post-initial-verification"
- Update BILL-03 requirement from "BLOCKED" to "SATISFIED" with note "Fixed: invoices now created as SENT"
- Update the Anti-Patterns section to mark the billing-service.ts issue as resolved
- Update the Gaps Summary section to note that the gap was resolved
- Add a re-verification note: "Re-verified: 2026-03-05 — gap closure confirmed"
3. Update the Score line in the body from "4/5" to "5/5".
Keep all other content intact. This is a documentation correction, not a rewrite.
</action>
<verify>
1. grep "status: passed" .planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md — should match
2. grep "5/5" .planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md — should match
3. grep "BLOCKED" .planning/phases/02-subscriber-and-billing-core/02-VERIFICATION.md — should return ZERO matches
</verify>
<done>Phase 2 VERIFICATION.md shows status: passed, score: 5/5, truth #3 marked as verified/fixed, BILL-03 satisfied, key link wired. Document accurately reflects the actual system state.</done>
</task>
</tasks>
<verification>
1. All E2E tests pass: npx jest --testPathPattern="e2e-workflows" --verbose
2. Full test suite passes: npx jest
3. Phase 2 VERIFICATION.md shows passed status with 5/5 score
4. New E2E workflows cover inventory receiving, expense recording, and portal ticket creation
</verification>
<success_criteria>
- E2E test coverage now includes Phase 4 inventory/expense workflows
- E2E test coverage now includes portal ticket creation flow
- Phase 2 VERIFICATION.md accurately reflects the fixed state
- All existing tests continue to pass
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-07-SUMMARY.md`
</output>