docs(05): create phase plan

Phase 05: Visibility and Client Portal
- 5 plans in 3 waves
- 2 parallel (wave 1), 1 sequential (wave 2), 2 parallel (wave 3)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kevin-asprec
2026-03-05 17:14:54 +08:00
parent 5276f3bbf4
commit ea58620c7d
6 changed files with 929 additions and 6 deletions

View File

@@ -114,14 +114,14 @@ Plans:
3. A subscriber can submit a support ticket through the client portal and see it reflected in staff's ticket queue 3. A subscriber can submit a support ticket through the client portal and see it reflected in staff's ticket queue
4. All API endpoints have integration tests that assert correct responses for authorized and unauthorized roles, verifying API-layer RBAC is not bypassed 4. All API endpoints have integration tests that assert correct responses for authorized and unauthorized roles, verifying API-layer RBAC is not bypassed
5. Critical user workflows (subscriber registration → invoice generation → payment recording, collector collection → remittance verification, ticket creation → job order completion) pass end-to-end tests 5. Critical user workflows (subscriber registration → invoice generation → payment recording, collector collection → remittance verification, ticket creation → job order completion) pass end-to-end tests
**Plans**: TBD **Plans**: 5 plans
Plans: Plans:
- [ ] 05-01: Dashboard service revenue collected, overdue counts, subscriber status counts, cash flow summary; pre-aggregated metrics with indexed queries (DASH-01, DASH-02, DASH-03, DASH-04) - [ ] 05-01-PLAN.md — Dashboard service: revenue metrics, overdue counts, subscriber status breakdown, cash flow summary, collector summary (DASH-01, DASH-02, DASH-03, DASH-04)
- [ ] 05-02: Client portal — subscriber login, bill and balance view, payment history, plan details and account status (PORT-01, PORT-02, PORT-04) - [ ] 05-02-PLAN.md — Portal auth and account view: subscriber login via account number, bill/balance view, payment history, plan details (PORT-01, PORT-02, PORT-04)
- [ ] 05-03: Client portal ticket submission and online payment scaffold ticket submission from portal, PORT-05 online payment interface (payment gateway integration deferred to v2 per project scope; scaffold only) (PORT-03, PORT-05) - [ ] 05-03-PLAN.md — Portal tickets and payment scaffold: ticket submission with conversation threads, online payment "coming soon" page (PORT-03, PORT-05)
- [ ] 05-04: Integration tests API endpoint tests for all roles, unauthorized access assertions, two-tenant cross-contamination tests (INFRA-03) - [ ] 05-04-PLAN.md — Integration tests: API RBAC enforcement for all 5 roles, unauthorized access assertions, two-tenant isolation tests (INFRA-03)
- [ ] 05-05: End-to-end tests — critical workflow automation: subscriber registration, billing cycle, payment recording, collector remittance, ticket-to-job-order resolution (INFRA-04) - [ ] 05-05-PLAN.md — End-to-end tests: billing workflow, collection/remittance workflow, ticket-to-job-order workflow (INFRA-04)
--- ---

View File

@@ -0,0 +1,160 @@
---
phase: 05-visibility-and-client-portal
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/services/dashboard-service.ts
- src/app/api/dashboard/route.ts
- src/lib/__tests__/dashboard-service.test.ts
autonomous: true
must_haves:
truths:
- "Dashboard returns revenue collected today and this month"
- "Dashboard returns overdue subscriber count and total outstanding amount"
- "Dashboard returns active/suspended/cancelled subscriber counts"
- "Dashboard returns cash flow summary (money in vs money out)"
- "Dashboard returns collector summary (today's collections, unverified remittances)"
artifacts:
- path: "src/lib/services/dashboard-service.ts"
provides: "DashboardService with all metric aggregation methods"
exports: ["getRevenueMetrics", "getSubscriberMetrics", "getOverdueMetrics", "getCashFlowSummary", "getCollectorSummary", "getDashboardSummary"]
- path: "src/app/api/dashboard/route.ts"
provides: "GET endpoint returning all dashboard metrics"
exports: ["GET"]
- path: "src/lib/__tests__/dashboard-service.test.ts"
provides: "Integration tests verifying metric accuracy"
key_links:
- from: "src/lib/services/dashboard-service.ts"
to: "prisma"
via: "aggregate queries on Payment, Invoice, Subscriber, Collection, Remittance, JournalEntryLine"
pattern: "prisma\\.(payment|invoice|subscriber|collection|remittance|journalEntryLine)\\."
- from: "src/app/api/dashboard/route.ts"
to: "src/lib/services/dashboard-service.ts"
via: "import and call getDashboardSummary"
pattern: "getDashboardSummary"
---
<objective>
Build the DashboardService that aggregates financial and operational metrics for the ISP owner dashboard, plus a GET /api/dashboard endpoint and integration tests.
Purpose: DASH-01, DASH-02, DASH-03, DASH-04 — the ISP owner's single-page business health overview.
Output: DashboardService, API route, passing 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/05-visibility-and-client-portal/05-CONTEXT.md
@src/lib/services/financial-report-service.ts
@src/lib/services/outstanding-report-service.ts
@src/lib/services/collection-report-service.ts
@src/lib/services/expense-report-service.ts
@src/lib/services/payment-service.ts
@src/lib/services/collector-service.ts
@src/lib/middleware/authorize.ts
@prisma/schema.prisma
</context>
<tasks>
<task type="auto">
<name>Task 1: DashboardService with metric aggregation</name>
<files>src/lib/services/dashboard-service.ts</files>
<action>
Create DashboardService with these methods, all accepting a TenantPrismaClient (same pattern as all other services):
1. **getRevenueMetrics(db, tenantId)** — Revenue collected today and this month (DASH-01):
- Query Payment table with status COMPLETED, grouped by date range
- `revenueToday`: sum of payments where createdAt is today (midnight to now)
- `revenueThisMonth`: sum of payments where createdAt is current month
- Return `{ revenueToday: Decimal, revenueThisMonth: Decimal }`
2. **getOverdueMetrics(db, tenantId)** — Overdue subscriber count and outstanding total (DASH-02):
- Query Invoice table for OVERDUE status invoices
- Count distinct subscriberIds with overdue invoices
- Sum (totalAmount - amountPaid) for all overdue invoices
- Return `{ overdueCount: number, totalOutstanding: Decimal }`
3. **getSubscriberMetrics(db, tenantId)** — Status breakdown (DASH-03):
- Query Subscriber table grouped by status
- Return `{ active: number, suspended: number, cancelled: number, total: number }`
4. **getCashFlowSummary(db, tenantId, startDate, endDate)** — Money in vs money out (DASH-04):
- Money in: sum of POSTED JE lines where account is revenue-type (4xxx codes) — use debit/credit with normal balance logic, same as FinancialReportService
- Money out: sum of POSTED JE lines where account is expense-type (5xxx codes)
- Return `{ moneyIn: Decimal, moneyOut: Decimal, netCashFlow: Decimal }`
- Default date range: current month
5. **getCollectorSummary(db, tenantId)** — Today's collections and unverified remittances (from CONTEXT.md):
- `collectionsToday`: sum of non-voided Collection amounts where createdAt is today
- `unverifiedRemittances`: count of Remittance records with status SUBMITTED (not VERIFIED)
- Return `{ collectionsToday: Decimal, unverifiedRemittances: number }`
6. **getDashboardSummary(db, tenantId)** — Aggregator that calls all five methods above and returns a single object.
Use Prisma aggregate/groupBy for efficiency. Follow the existing TenantPrismaClient pattern (type as any). Use Decimal from Prisma for all money fields. All date comparisons use UTC.
</action>
<verify>TypeScript compiles: `npx tsc --noEmit src/lib/services/dashboard-service.ts` (or full project compile)</verify>
<done>DashboardService exports all 6 methods with correct return types</done>
</task>
<task type="auto">
<name>Task 2: Dashboard API route and integration tests</name>
<files>src/app/api/dashboard/route.ts, src/lib/__tests__/dashboard-service.test.ts</files>
<action>
**API Route (src/app/api/dashboard/route.ts):**
- GET handler wrapped with `withPermission("read", "Report")` — ADMIN and OFFICE_STAFF have this permission
- Calls getDashboardSummary with tenant-scoped Prisma client
- Returns JSON with all metrics
- Accepts optional query params: `startDate`, `endDate` for cash flow date range (defaults to current month)
- Follow existing route patterns (see any route.ts in src/app/api/)
**Integration Tests (src/lib/__tests__/dashboard-service.test.ts):**
Create tests using vitest with live PostgreSQL (same pattern as all other test files):
Setup: Create tenant, seed COA, create subscriber, generate invoice, record payment, create collection, create remittance.
Tests (minimum 6):
1. `getRevenueMetrics returns today's revenue` — after recording a payment, revenueToday > 0
2. `getOverdueMetrics counts overdue invoices` — create invoice with past due date, verify overdueCount = 1
3. `getSubscriberMetrics returns status breakdown` — create active + suspended subscribers, verify counts
4. `getCashFlowSummary computes net cash flow` — after payment + expense, verify moneyIn and moneyOut
5. `getCollectorSummary returns today's collections` — after collection, verify collectionsToday > 0
6. `getDashboardSummary aggregates all metrics` — verify the composite object has all fields
Cleanup order: follow the most comprehensive cleanup pattern from 04-04 (expense report tests) since dashboard touches payments, invoices, collections, remittances, expenses, and JEs.
Run tests with: `npx vitest run src/lib/__tests__/dashboard-service.test.ts`
</action>
<verify>`npx vitest run src/lib/__tests__/dashboard-service.test.ts` — all tests pass</verify>
<done>GET /api/dashboard returns all dashboard metrics; 6+ tests pass verifying metric accuracy</done>
</task>
</tasks>
<verification>
- `npx tsc --noEmit` passes (no type errors)
- `npx vitest run src/lib/__tests__/dashboard-service.test.ts` — all tests pass
- DashboardService correctly derives all metrics from existing data (no new stored balances)
</verification>
<success_criteria>
- DashboardService aggregates revenue, overdue, subscriber status, cash flow, and collector metrics
- GET /api/dashboard returns the composite dashboard object
- All integration tests pass, verifying metric accuracy against seeded data
- DASH-01, DASH-02, DASH-03, DASH-04 requirements satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,196 @@
---
phase: 05-visibility-and-client-portal
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/auth-options.ts
- src/lib/services/portal-service.ts
- src/app/api/portal/auth/route.ts
- src/app/api/portal/account/route.ts
- src/app/api/portal/invoices/route.ts
- src/app/api/portal/payments/route.ts
- src/middleware.ts
- src/lib/__tests__/portal-service.test.ts
autonomous: true
must_haves:
truths:
- "Subscriber can log in with account number and password"
- "Subscriber can view their current bill and outstanding balance"
- "Subscriber can view their full payment history"
- "Subscriber can view their current plan details and account status"
- "Portal API endpoints are scoped to the logged-in subscriber only"
artifacts:
- path: "src/lib/services/portal-service.ts"
provides: "Subscriber-scoped data retrieval for portal"
exports: ["getPortalAccount", "getPortalInvoices", "getPortalPayments"]
- path: "src/app/api/portal/account/route.ts"
provides: "GET endpoint for subscriber account overview"
exports: ["GET"]
- path: "src/app/api/portal/invoices/route.ts"
provides: "GET endpoint for subscriber invoices"
exports: ["GET"]
- path: "src/app/api/portal/payments/route.ts"
provides: "GET endpoint for subscriber payment history"
exports: ["GET"]
key_links:
- from: "src/lib/auth-options.ts"
to: "prisma.subscriber"
via: "portal credentials provider lookup by accountNumber"
pattern: "accountNumber"
- from: "src/app/api/portal/account/route.ts"
to: "src/lib/services/portal-service.ts"
via: "getPortalAccount call"
pattern: "getPortalAccount"
- from: "src/middleware.ts"
to: "/portal"
via: "exclude portal/login from auth requirement"
pattern: "portal"
---
<objective>
Build subscriber portal authentication (login via account number) and read-only account/billing/payment endpoints scoped to the logged-in subscriber.
Purpose: PORT-01, PORT-02, PORT-04 — subscribers can self-serve to view their bills, payments, and plan details.
Output: Portal auth provider, PortalService, portal API routes, passing 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/05-visibility-and-client-portal/05-CONTEXT.md
@src/lib/auth-options.ts
@src/lib/middleware/authorize.ts
@src/lib/services/subscriber-service.ts
@src/lib/services/invoice-service.ts
@src/lib/services/payment-service.ts
@src/middleware.ts
@src/lib/casl/permissions.ts
@prisma/schema.prisma
</context>
<tasks>
<task type="auto">
<name>Task 1: Portal authentication — subscriber login via account number</name>
<files>src/lib/auth-options.ts, src/middleware.ts, prisma/schema.prisma</files>
<action>
**Schema change (prisma/schema.prisma):**
Add a `passwordHash` field to the Subscriber model:
```
passwordHash String?
```
This is nullable because existing subscribers may not have portal access yet. Only subscribers with a passwordHash can log in.
Run `npx prisma db push` to apply (same pattern as all prior schema changes).
**Auth options (src/lib/auth-options.ts):**
Add a SECOND CredentialsProvider to the providers array with id `"portal-credentials"`:
- Accepts `{ accountNumber, password, tenantId }` (tenantId is required to scope the lookup — subscriber account numbers are unique per tenant)
- Looks up Subscriber by `@@unique([tenantId, accountNumber])` where passwordHash is not null
- Verifies password with bcrypt.compare
- On success, returns a user-shaped object with:
- `id`: subscriber.id
- `email`: subscriber.email || subscriber.accountNumber (NextAuth requires email field)
- `tenantId`: subscriber.tenantId
- `roles`: [Role.CLIENT]
- `isSuperAdmin`: false
- `firstName`: subscriber.firstName
- `lastName`: subscriber.lastName
- Plus a custom field `subscriberId`: subscriber.id (to distinguish portal users from staff users)
- The JWT callback must persist `subscriberId` into the token
- The session callback must expose `subscriberId` on session.user
**Important:** Keep the existing staff CredentialsProvider unchanged. The portal provider is additive.
**NextAuth type extension (src/types/next-auth.d.ts):**
Add `subscriberId?: string` to the Session user interface and JWT interface.
**Middleware (src/middleware.ts):**
Add `/portal/login` to the matcher exclusion pattern so unauthenticated subscribers can reach the login page. Pattern update: add `portal/login` to the negative lookahead alongside `login|signup`.
Also exclude `/api/portal/auth` from the auth requirement so the portal login API call works.
</action>
<verify>`npx prisma db push` succeeds; `npx tsc --noEmit` compiles without errors</verify>
<done>Subscriber can authenticate via account number + password through NextAuth portal-credentials provider; JWT carries subscriberId; middleware allows portal login page access</done>
</task>
<task type="auto">
<name>Task 2: PortalService and portal API endpoints with tests</name>
<files>src/lib/services/portal-service.ts, src/app/api/portal/account/route.ts, src/app/api/portal/invoices/route.ts, src/app/api/portal/payments/route.ts, src/lib/__tests__/portal-service.test.ts</files>
<action>
**PortalService (src/lib/services/portal-service.ts):**
All methods take a TenantPrismaClient and subscriberId — data is ALWAYS scoped to that single subscriber.
1. **getPortalAccount(db, subscriberId)** — Returns subscriber profile with plan details:
- Query Subscriber with include: { servicePlan: true }
- Return: { accountNumber, firstName, lastName, email, phone, address, status, activatedAt, plan: { name, speed, monthlyPrice, billingType }, creditBalance, billingDay }
2. **getPortalInvoices(db, subscriberId, options?: { page, limit })** — Returns paginated invoices:
- Query Invoice where subscriberId, ordered by periodStart DESC
- Include invoiceLines for detail
- Return: { invoices: Invoice[], total: number, page: number }
- Default: page=1, limit=10
3. **getPortalPayments(db, subscriberId, options?: { page, limit })** — Returns paginated payment history:
- Query Payment where subscriberId, ordered by createdAt DESC
- Return: { payments: Payment[], total: number, page: number }
- Default: page=1, limit=20
**API Routes:**
All portal API routes use a custom `withPortalAuth` helper (create it in the same file or a shared portal middleware file). This helper:
- Gets the session via getCurrentUser()
- Checks session.user.subscriberId exists (must be a portal user, not staff)
- Returns 401 if no session, 403 if not a portal user
- Passes subscriberId to the handler
**GET /api/portal/account** — calls getPortalAccount, returns subscriber profile + plan
**GET /api/portal/invoices** — calls getPortalInvoices with pagination from query params
**GET /api/portal/payments** — calls getPortalPayments with pagination from query params
**Integration Tests (src/lib/__tests__/portal-service.test.ts):**
Setup: Create tenant, create subscriber with passwordHash (use bcrypt.hashSync), create service plan, generate invoices, record payments.
Tests (minimum 5):
1. `getPortalAccount returns subscriber with plan details` — verify all fields present
2. `getPortalInvoices returns paginated invoices` — create 3 invoices, request page 1 limit 2, verify pagination
3. `getPortalPayments returns paginated payment history` — create 2 payments, verify list
4. `getPortalAccount scoped to subscriberId only` — create 2 subscribers, verify each can only see their own data
5. `getPortalInvoices includes invoice line items` — verify invoiceLines are included
Cleanup: payments -> paymentAllocations -> invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> users -> tenant
</action>
<verify>`npx vitest run src/lib/__tests__/portal-service.test.ts` — all tests pass</verify>
<done>Portal endpoints return subscriber-scoped account, invoice, and payment data; 5+ tests pass; PORT-01, PORT-02, PORT-04 satisfied</done>
</task>
</tasks>
<verification>
- `npx prisma db push` applies Subscriber.passwordHash
- `npx tsc --noEmit` passes
- `npx vitest run src/lib/__tests__/portal-service.test.ts` — all tests pass
- Portal auth uses account number (not email) per CONTEXT.md decision
</verification>
<success_criteria>
- Subscriber authenticates via account number + password
- Portal API endpoints return only the logged-in subscriber's data
- Account overview includes plan details, balance, and billing day
- Invoice and payment history are paginated
- PORT-01, PORT-02, PORT-04 requirements satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,214 @@
---
phase: 05-visibility-and-client-portal
plan: 03
type: execute
wave: 2
depends_on: ["05-02"]
files_modified:
- prisma/schema.prisma
- src/lib/services/portal-ticket-service.ts
- src/app/api/portal/tickets/route.ts
- src/app/api/portal/tickets/[id]/route.ts
- src/app/api/portal/tickets/[id]/comments/route.ts
- src/app/api/portal/payments/coming-soon/route.ts
- src/lib/__tests__/portal-ticket-service.test.ts
autonomous: true
must_haves:
truths:
- "Subscriber can submit a support ticket from the portal"
- "Subscriber can view their ticket list with current status"
- "Subscriber can add follow-up comments to open tickets"
- "Subscriber's ticket appears in staff's ticket queue"
- "Online payment page shows outstanding balance and alternative payment instructions"
artifacts:
- path: "prisma/schema.prisma"
provides: "TicketComment model for conversation threads"
contains: "model TicketComment"
- path: "src/lib/services/portal-ticket-service.ts"
provides: "Subscriber-scoped ticket creation and commenting"
exports: ["createPortalTicket", "listPortalTickets", "getPortalTicket", "addTicketComment"]
- path: "src/app/api/portal/tickets/route.ts"
provides: "GET (list) and POST (create) for portal tickets"
exports: ["GET", "POST"]
- path: "src/app/api/portal/tickets/[id]/comments/route.ts"
provides: "GET (list) and POST (add) for ticket comments"
exports: ["GET", "POST"]
- path: "src/app/api/portal/payments/coming-soon/route.ts"
provides: "GET endpoint returning balance and payment instructions"
exports: ["GET"]
key_links:
- from: "src/lib/services/portal-ticket-service.ts"
to: "src/lib/services/ticket-service.ts"
via: "delegates ticket creation to existing createTicket"
pattern: "createTicket"
- from: "src/app/api/portal/tickets/route.ts"
to: "src/lib/services/portal-ticket-service.ts"
via: "createPortalTicket and listPortalTickets calls"
pattern: "(createPortalTicket|listPortalTickets)"
---
<objective>
Build portal ticket submission with conversation threads, and scaffold the online payment "coming soon" page with outstanding balance display.
Purpose: PORT-03 (ticket submission) and PORT-05 (online payment scaffold). Subscribers can interact with support through the portal.
Output: TicketComment model, portal ticket APIs, payment scaffold endpoint, passing 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/05-visibility-and-client-portal/05-CONTEXT.md
@.planning/phases/05-visibility-and-client-portal/05-02-SUMMARY.md
@src/lib/services/ticket-service.ts
@src/lib/services/portal-service.ts
@src/lib/casl/permissions.ts
@prisma/schema.prisma
</context>
<tasks>
<task type="auto">
<name>Task 1: TicketComment model and portal ticket service</name>
<files>prisma/schema.prisma, src/lib/services/portal-ticket-service.ts</files>
<action>
**Schema (prisma/schema.prisma):**
Add TicketComment model for conversation threads:
```prisma
model TicketComment {
id String @id @default(uuid())
tenantId String
ticketId String
ticket Ticket @relation(fields: [ticketId], references: [id])
/// Who posted this comment — can be subscriber (CLIENT) or staff
authorId String
author User @relation(fields: [authorId], references: [id])
/// For portal comments, link to the subscriber directly
subscriberId String?
message String
createdAt DateTime @default(now())
@@index([tenantId])
@@index([ticketId])
}
```
Add `comments TicketComment[]` relation to the Ticket model.
Add `ticketComments TicketComment[]` relation to the User model.
Run `npx prisma db push`.
**PortalTicketService (src/lib/services/portal-ticket-service.ts):**
1. **createPortalTicket(db, tenantId, subscriberId, data: { categoryId, subject, description })**
- Look up the subscriber to get their User record (if they have one via the portal auth) OR use the subscriberId for the createdById field
- Important: The subscriber may not have a User record. For portal ticket creation, we need a userId. Two approaches:
- Option A (preferred): When portal auth creates a session, the session has a userId. Use that.
- Option B: Create a shadow User record for the subscriber. Too complex.
- Actually, re-examine: the existing Ticket model requires `createdById -> User.id`. But portal subscribers authenticate via Subscriber, not User. Resolution: When a subscriber gets portal access (passwordHash set), also create a corresponding User record with Role.CLIENT. This should be done in plan 02's auth setup. If plan 02 already handles this, use the userId from the session. If not, this task must handle it.
- Simpler approach: Portal auth returns a user-like session. The `subscriberId` in the JWT IS the subscriber's ID. For ticket creation, use `createdById` as the session user's ID. But the session user for portal is mapped from Subscriber, not User table.
- **Resolution:** Create portal tickets using the existing `createTicket` from ticket-service.ts, but pass `source: 'PORTAL'` (TicketSource.PORTAL exists in the enum). The `createdById` needs a valid User.id. So portal subscribers MUST have a corresponding User record with CLIENT role. Add this to the portal auth flow: when a subscriber logs in for the first time, ensure a User record exists with their info and CLIENT role. Use `upsert` by subscriber email or a portal-specific identifier.
- Delegate actual ticket creation to existing `createTicket()` from ticket-service.ts with source=PORTAL
- Return the created ticket
2. **listPortalTickets(db, subscriberId, options?: { page, limit })**
- Query Ticket where subscriberId, ordered by createdAt DESC
- Include: category (name), latest comment
- Return paginated list with status
3. **getPortalTicket(db, subscriberId, ticketId)**
- Get single ticket with ALL comments (conversation thread)
- Verify ticket.subscriberId === subscriberId (security check)
- Include: category, all comments ordered by createdAt ASC, each comment with author name
- Return ticket with comments array
4. **addTicketComment(db, tenantId, subscriberId, ticketId, message)**
- Verify ticket exists AND ticket.subscriberId === subscriberId
- Verify ticket is not CLOSED (cannot comment on closed tickets)
- Create TicketComment with subscriberId set
- Return the created comment
**User record for portal subscribers:**
Add a helper `ensurePortalUser(db, tenantId, subscriber)` that:
- Checks if a User with email=subscriber.accountNumber (or a portal-specific convention) and tenantId exists
- If not, creates one with: email=`portal-{accountNumber}@portal.local`, roles=[CLIENT], firstName=subscriber.firstName, lastName=subscriber.lastName, passwordHash=subscriber.passwordHash
- Returns the User.id
- Call this during portal authentication (in auth-options.ts portal provider) or lazily on first ticket creation
</action>
<verify>`npx prisma db push` succeeds; `npx tsc --noEmit` compiles</verify>
<done>TicketComment model exists; PortalTicketService creates tickets via existing ticket-service, supports conversation threads</done>
</task>
<task type="auto">
<name>Task 2: Portal ticket API routes, payment scaffold, and tests</name>
<files>src/app/api/portal/tickets/route.ts, src/app/api/portal/tickets/[id]/route.ts, src/app/api/portal/tickets/[id]/comments/route.ts, src/app/api/portal/payments/coming-soon/route.ts, src/lib/__tests__/portal-ticket-service.test.ts</files>
<action>
**Portal Ticket API Routes:**
All routes use the `withPortalAuth` helper from plan 02 (or re-implement the same pattern: check session.subscriberId exists).
**GET /api/portal/tickets** — List subscriber's tickets (paginated)
**POST /api/portal/tickets** — Create a new ticket: accepts `{ categoryId, subject, description }`
**GET /api/portal/tickets/[id]** — Get ticket detail with conversation thread
**GET /api/portal/tickets/[id]/comments** — List comments for a ticket
**POST /api/portal/tickets/[id]/comments** — Add a comment: accepts `{ message }`
For [id] routes, use the dynamic route handler pattern from Phase 2+:
```typescript
export function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
// await params, then call service
}
```
**Payment Scaffold (src/app/api/portal/payments/coming-soon/route.ts):**
GET endpoint using withPortalAuth:
- Query subscriber's outstanding balance (sum of unpaid invoice amounts)
- Query tenant settings for payment instructions (add `paymentInstructions` text field to TenantSettings if not present — nullable String?)
- Return: `{ outstandingBalance: Decimal, paymentInstructions: string | null, message: "Online payments coming soon" }`
If TenantSettings needs a new field, add it to schema.prisma and run `npx prisma db push`.
**Integration Tests (src/lib/__tests__/portal-ticket-service.test.ts):**
Setup: Create tenant, create subscriber with passwordHash, ensure portal User record exists, seed ticket categories.
Tests (minimum 6):
1. `createPortalTicket creates ticket with PORTAL source` — verify ticket.source === 'PORTAL'
2. `createPortalTicket ticket appears in staff listTickets` — create via portal, query via staff service, verify it exists
3. `listPortalTickets returns only subscriber's tickets` — create tickets for 2 subscribers, verify isolation
4. `addTicketComment creates conversation entry` — add comment, verify it's linked to ticket
5. `addTicketComment rejects on closed ticket` — close ticket, try to comment, expect error
6. `getPortalTicket returns ticket with full conversation thread` — add 3 comments, verify all returned in order
Cleanup: ticketComments -> tickets -> ticketCategories -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> users -> tenant
</action>
<verify>`npx vitest run src/lib/__tests__/portal-ticket-service.test.ts` — all tests pass</verify>
<done>Subscribers can create tickets, view ticket list, have conversations on tickets; payment scaffold shows balance; 6+ tests pass; PORT-03 and PORT-05 satisfied</done>
</task>
</tasks>
<verification>
- `npx prisma db push` applies TicketComment model and any TenantSettings additions
- `npx tsc --noEmit` passes
- `npx vitest run src/lib/__tests__/portal-ticket-service.test.ts` — all tests pass
- Portal ticket appears in staff's existing ticket query (cross-system verification)
</verification>
<success_criteria>
- Subscriber creates ticket from portal with PORTAL source
- Ticket conversation thread supports subscriber and staff messages
- Closed tickets reject new comments
- Payment scaffold returns outstanding balance and instructions
- PORT-03, PORT-05 requirements satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,141 @@
---
phase: 05-visibility-and-client-portal
plan: 04
type: execute
wave: 3
depends_on: ["05-01", "05-02", "05-03"]
files_modified:
- src/lib/__tests__/integration/api-rbac.test.ts
autonomous: true
must_haves:
truths:
- "All API endpoints return 401 for unauthenticated requests"
- "Role-restricted endpoints return 403 for unauthorized roles"
- "Tenant A data is never returned to Tenant B users"
- "COLLECTOR cannot access billing or subscriber management write endpoints"
- "TECHNICIAN cannot access payment or invoice endpoints"
- "CLIENT role can only read their own data"
artifacts:
- path: "src/lib/__tests__/integration/api-rbac.test.ts"
provides: "Comprehensive API RBAC integration tests"
min_lines: 200
key_links:
- from: "src/lib/__tests__/integration/api-rbac.test.ts"
to: "src/lib/middleware/authorize.ts"
via: "tests verify withPermission enforcement"
pattern: "withPermission|401|403"
---
<objective>
Create integration tests that verify API-layer RBAC enforcement across all endpoints, including unauthorized access assertions and two-tenant cross-contamination tests.
Purpose: INFRA-03 — prove that the authorization layer cannot be bypassed at the API level.
Output: Comprehensive integration test suite covering all roles and endpoints.
</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
@src/lib/middleware/authorize.ts
@src/lib/casl/permissions.ts
@src/lib/casl/types.ts
@src/middleware.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: API RBAC integration test suite</name>
<files>src/lib/__tests__/integration/api-rbac.test.ts</files>
<action>
Create a comprehensive integration test file that tests API-layer authorization. Since Next.js API routes cannot be easily called via HTTP in test mode without starting the server, test at the SERVICE + MIDDLEWARE layer instead — test the withPermission HOF behavior and service-level tenant isolation.
**Approach:** Test the authorization logic directly by:
1. Creating users with different roles in the test database
2. Calling service methods with different tenant contexts
3. Verifying CASL ability checks for each role against each subject
**Test structure:**
```
describe("API RBAC Integration Tests")
describe("Authentication (401)")
- Verify getCurrentUser returns null for no session -> would produce 401
- Verify withPermission returns 401 when no user (mock getCurrentUser to return null)
describe("Authorization by Role (403)")
For each role, test what they CAN and CANNOT access:
describe("ADMIN")
- Can access all subjects (manage all)
describe("OFFICE_STAFF")
- Can manage Subscriber, Invoice, Payment, Ticket, JobOrder, Inventory, Expense, Vendor
- Can read Report, Account
- CANNOT create/update/delete Account
describe("COLLECTOR")
- Can read Subscriber, Zone
- Can create Payment, read Payment
- CANNOT read Report, manage Invoice, manage Subscriber, manage Ticket
describe("TECHNICIAN")
- Can read/update JobOrder (own only)
- Can read Subscriber, read Inventory (own only)
- CANNOT manage Payment, Invoice, Report, Ticket
describe("CLIENT")
- Can read Invoice (own), Payment (own), Subscriber (own)
- Can create Ticket, read Ticket (own)
- CANNOT manage User, read Report, manage Subscriber
describe("Tenant Isolation")
- Create Tenant A and Tenant B with subscribers, invoices, payments
- Using Tenant A's context, query subscribers -> returns ONLY Tenant A data
- Using Tenant B's context, query subscribers -> returns ONLY Tenant B data
- Cross-query: Tenant A's subscriber ID passed to Tenant B context -> returns null or throws
- Verify: invoice created in Tenant A is invisible to Tenant B
- Verify: payment recorded in Tenant A is invisible to Tenant B
```
**Implementation details:**
- Use `defineAbilityFor` and `definePermissionsFor` directly to test CASL rules
- Use `withTenantContext` to create tenant-scoped Prisma clients for isolation tests
- Use real database with actual tenant/user/subscriber records
- Test at least 5 role combinations with at least 3 subjects each = 15+ assertions
- Test at least 3 cross-tenant scenarios
**Cleanup order:** Follow the most comprehensive pattern (expense report cleanup from 04-04) since we create data across multiple subsystems.
Minimum test count: 15+ tests across all describe blocks.
</action>
<verify>`npx vitest run src/lib/__tests__/integration/api-rbac.test.ts` — all tests pass</verify>
<done>All 5 roles tested against all relevant subjects; cross-tenant isolation verified with real data; 15+ tests pass; INFRA-03 satisfied</done>
</task>
</tasks>
<verification>
- `npx vitest run src/lib/__tests__/integration/api-rbac.test.ts` — all tests pass
- Every role's can/cannot boundaries are explicitly tested
- Two-tenant cross-contamination test proves data isolation
</verification>
<success_criteria>
- 15+ integration tests covering all 5 roles
- Unauthorized access assertions prove RBAC enforcement
- Two-tenant isolation tests prove no cross-contamination
- INFRA-03 requirement satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-04-SUMMARY.md`
</output>

View File

@@ -0,0 +1,212 @@
---
phase: 05-visibility-and-client-portal
plan: 05
type: execute
wave: 3
depends_on: ["05-01", "05-02", "05-03"]
files_modified:
- src/lib/__tests__/integration/e2e-workflows.test.ts
autonomous: true
must_haves:
truths:
- "Subscriber registration through invoice generation through payment recording works as a single workflow"
- "Collector collection through remittance verification works end-to-end with correct JE postings"
- "Ticket creation through job order completion triggers auto-resolve"
- "All JEs produced during workflows are balanced (debits = credits)"
- "Data created by workflows appears correctly in dashboard metrics"
artifacts:
- path: "src/lib/__tests__/integration/e2e-workflows.test.ts"
provides: "End-to-end workflow tests for critical business processes"
min_lines: 250
key_links:
- from: "src/lib/__tests__/integration/e2e-workflows.test.ts"
to: "src/lib/services/*.ts"
via: "orchestrates multiple services in sequence"
pattern: "(SubscriberService|BillingService|PaymentService|CollectorService|RemittanceService|TicketService|JobOrderService|DashboardService)"
---
<objective>
Create end-to-end tests that exercise critical user workflows from start to finish, verifying that all services integrate correctly and produce accurate accounting entries.
Purpose: INFRA-04 — prove that the system works as a coherent whole, not just isolated units.
Output: E2E workflow test suite covering the three critical business processes.
</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
@src/lib/services/subscriber-service.ts
@src/lib/services/billing-service.ts
@src/lib/services/payment-service.ts
@src/lib/services/collector-service.ts
@src/lib/services/remittance-service.ts
@src/lib/services/ticket-service.ts
@src/lib/services/job-order-service.ts
@src/lib/services/dashboard-service.ts
@src/lib/accounting/journal-entry-service.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Billing workflow end-to-end test</name>
<files>src/lib/__tests__/integration/e2e-workflows.test.ts</files>
<action>
Create the e2e test file with the first critical workflow.
**Setup (shared across all workflows in this file):**
- Create tenant (triggers COA seed, ticket category seed)
- Create admin user, office staff user, collector user, technician user
- Create service plan
- Create zone, assign collector to zone
**Workflow 1: Subscriber Registration -> Invoice Generation -> Payment Recording**
Test as a sequential story:
```
describe("E2E: Billing Workflow")
test("1. Register a new subscriber")
- Call createSubscriber with name, address, plan
- Verify: subscriber created with ACTIVE status, accountNumber generated (SUB-NNNN)
- Verify: subscriber has the correct plan assigned
test("2. Generate invoice for subscriber")
- Call generateInvoiceForSubscriber (from BillingService)
- Verify: invoice created with correct totalAmount matching plan price
- Verify: invoice has UNPAID status
- Verify: JE posted (DR 1100 AR, CR 4010 Service Revenue) — verify via JournalEntryService.getTrialBalance or direct query
test("3. Record full payment against invoice")
- Call recordPayment with full invoice amount
- Verify: invoice status = PAID, amountPaid = totalAmount
- Verify: payment created with COMPLETED status
- Verify: JE posted (DR 1010 Cash, CR 1100 AR)
test("4. Record partial payment on second invoice")
- Generate second invoice
- Record partial payment (50% of amount)
- Verify: invoice status = PARTIAL, amountPaid = partial amount
- Verify: JE for partial amount is balanced
test("5. Verify trial balance is balanced after all transactions")
- Call getTrialBalance
- Verify: totalDebits === totalCredits (books are self-verifying)
test("6. Dashboard reflects billing activity")
- Call getRevenueMetrics
- Verify: revenueToday includes the payments made
- Call getSubscriberMetrics
- Verify: active count includes the registered subscriber
```
Use the existing service functions directly (not HTTP calls). This tests the service integration layer.
</action>
<verify>`npx vitest run src/lib/__tests__/integration/e2e-workflows.test.ts` — billing workflow tests pass</verify>
<done>Billing workflow e2e test passes: subscriber -> invoice -> payment -> balanced books -> dashboard reflects</done>
</task>
<task type="auto">
<name>Task 2: Collection and ticket workflow end-to-end tests</name>
<files>src/lib/__tests__/integration/e2e-workflows.test.ts</files>
<action>
Add two more workflow test suites to the same file.
**Workflow 2: Collector Collection -> Remittance Verification**
```
describe("E2E: Collection & Remittance Workflow")
test("1. Collector records field collection")
- Create subscriber with unpaid invoice
- Call recordCollection (from CollectorService) as collector
- Verify: collection created, invoice allocated via FIFO
- Verify: JE posted (DR 1030 Cash in Transit, CR 1100 AR)
test("2. Collector submits remittance")
- Call createRemittance with the collection amount
- Verify: remittance created with SUBMITTED status
test("3. Office staff verifies remittance")
- Call verifyRemittance with verified total = submitted total
- Verify: remittance status = VERIFIED
- Verify: JE posted (DR 1010 Cash on Hand, CR 1030 Cash in Transit)
- Verify: variance = 0
test("4. Dashboard reflects collection activity")
- Call getCollectorSummary
- Verify: collectionsToday > 0
- Verify: unverifiedRemittances = 0 (all verified)
test("5. Trial balance still balanced after collection workflow")
- Verify totalDebits === totalCredits
```
**Workflow 3: Ticket Creation -> Job Order -> Completion -> Auto-Resolve**
```
describe("E2E: Ticket to Job Order Resolution")
test("1. Staff creates ticket from client call")
- Call createTicket with category, subject, description, source=STAFF
- Verify: ticket created with OPEN status, TKT-NNNN number
test("2. Convert ticket to job order")
- Call createJobOrder linked to ticket
- Assign to technician
- Verify: job order created with PENDING status
- Verify: ticket status auto-transitions to ASSIGNED
test("3. Technician completes job order")
- Call updateJobOrderStatus to IN_PROGRESS
- Call updateJobOrderStatus to COMPLETED with outcomeNotes
- Verify: job order status = COMPLETED
test("4. Ticket auto-resolves when all jobs complete")
- Verify: ticket status = RESOLVED (auto-resolved by checkTicketAutoResolve)
- Verify: resolvedAt is set
test("5. Staff closes resolved ticket")
- Call transitionTicketStatus to CLOSED
- Verify: ticket status = CLOSED
- Verify: closedAt is set
```
**Cleanup (afterAll):** Use the most comprehensive cleanup order covering all subsystems:
ticketComments -> jobOrders -> tickets -> ticketCategories -> collectionAllocations -> collections -> remittances -> paymentAllocations -> payments -> invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> stockMovements -> inventoryItems -> expenses -> vendors -> expenseCategories (custom) -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> zoneAssignments -> zones -> technicianProfiles -> jobTypeRates -> users -> tenant
Minimum total test count across all 3 workflows: 16+ tests.
</action>
<verify>`npx vitest run src/lib/__tests__/integration/e2e-workflows.test.ts` — all workflow tests pass</verify>
<done>All 3 critical workflows pass e2e: billing, collection/remittance, ticket/job-order; trial balance balanced throughout; dashboard metrics accurate; 16+ tests pass; INFRA-04 satisfied</done>
</task>
</tasks>
<verification>
- `npx vitest run src/lib/__tests__/integration/e2e-workflows.test.ts` — all tests pass
- Trial balance is balanced after each workflow (self-verifying books)
- Dashboard metrics reflect workflow activity
- All three critical paths exercised end-to-end
</verification>
<success_criteria>
- Billing workflow: registration -> invoice -> payment -> balanced books
- Collection workflow: field collection -> remittance -> verification -> balanced books
- Ticket workflow: create -> job order -> completion -> auto-resolve -> close
- Trial balance balanced after every workflow
- Dashboard metrics reflect real data from workflows
- INFRA-04 requirement satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-05-SUMMARY.md`
</output>