Files
kevin-asprec ea58620c7d 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>
2026-03-05 17:14:54 +08:00

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
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
true
truths artifacts key_links
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
path provides exports
src/lib/services/portal-service.ts Subscriber-scoped data retrieval for portal
getPortalAccount
getPortalInvoices
getPortalPayments
path provides exports
src/app/api/portal/account/route.ts GET endpoint for subscriber account overview
GET
path provides exports
src/app/api/portal/invoices/route.ts GET endpoint for subscriber invoices
GET
path provides exports
src/app/api/portal/payments/route.ts GET endpoint for subscriber payment history
GET
from to via pattern
src/lib/auth-options.ts prisma.subscriber portal credentials provider lookup by accountNumber accountNumber
from to via pattern
src/app/api/portal/account/route.ts src/lib/services/portal-service.ts getPortalAccount call getPortalAccount
from to via pattern
src/middleware.ts /portal exclude portal/login from auth requirement portal
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.

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

Task 2: PortalService and portal API endpoints with tests 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 **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 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

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

<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>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-02-SUMMARY.md`