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

@@ -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>