--- 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" --- 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. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.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 - 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 After completion, create `.planning/phases/05-visibility-and-client-portal/05-02-SUMMARY.md`