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>
8.9 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-visibility-and-client-portal | 02 | execute | 1 |
|
true |
|
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.
<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>
@.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
Task 1: Portal authentication — subscriber login via account number src/lib/auth-options.ts, src/middleware.ts, prisma/schema.prisma **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.idemail: subscriber.email || subscriber.accountNumber (NextAuth requires email field)tenantId: subscriber.tenantIdroles: [Role.CLIENT]isSuperAdmin: falsefirstName: subscriber.firstNamelastName: subscriber.lastName- Plus a custom field
subscriberId: subscriber.id (to distinguish portal users from staff users)
- The JWT callback must persist
subscriberIdinto the token - The session callback must expose
subscriberIdon 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.
npx prisma db push succeeds; npx tsc --noEmit compiles without errors
Subscriber can authenticate via account number + password through NextAuth portal-credentials provider; JWT carries subscriberId; middleware allows portal login page access
-
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 }
-
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
-
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):
getPortalAccount returns subscriber with plan details— verify all fields presentgetPortalInvoices returns paginated invoices— create 3 invoices, request page 1 limit 2, verify paginationgetPortalPayments returns paginated payment history— create 2 payments, verify listgetPortalAccount scoped to subscriberId only— create 2 subscribers, verify each can only see their own datagetPortalInvoices includes invoice line items— verify invoiceLines are included
Cleanup: payments -> paymentAllocations -> invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> users -> tenant
npx vitest run src/lib/__tests__/portal-service.test.ts — all tests pass
Portal endpoints return subscriber-scoped account, invoice, and payment data; 5+ tests pass; PORT-01, PORT-02, PORT-04 satisfied
<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>