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:
196
.planning/phases/05-visibility-and-client-portal/05-02-PLAN.md
Normal file
196
.planning/phases/05-visibility-and-client-portal/05-02-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user