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,214 @@
---
phase: 05-visibility-and-client-portal
plan: 03
type: execute
wave: 2
depends_on: ["05-02"]
files_modified:
- prisma/schema.prisma
- src/lib/services/portal-ticket-service.ts
- src/app/api/portal/tickets/route.ts
- src/app/api/portal/tickets/[id]/route.ts
- src/app/api/portal/tickets/[id]/comments/route.ts
- src/app/api/portal/payments/coming-soon/route.ts
- src/lib/__tests__/portal-ticket-service.test.ts
autonomous: true
must_haves:
truths:
- "Subscriber can submit a support ticket from the portal"
- "Subscriber can view their ticket list with current status"
- "Subscriber can add follow-up comments to open tickets"
- "Subscriber's ticket appears in staff's ticket queue"
- "Online payment page shows outstanding balance and alternative payment instructions"
artifacts:
- path: "prisma/schema.prisma"
provides: "TicketComment model for conversation threads"
contains: "model TicketComment"
- path: "src/lib/services/portal-ticket-service.ts"
provides: "Subscriber-scoped ticket creation and commenting"
exports: ["createPortalTicket", "listPortalTickets", "getPortalTicket", "addTicketComment"]
- path: "src/app/api/portal/tickets/route.ts"
provides: "GET (list) and POST (create) for portal tickets"
exports: ["GET", "POST"]
- path: "src/app/api/portal/tickets/[id]/comments/route.ts"
provides: "GET (list) and POST (add) for ticket comments"
exports: ["GET", "POST"]
- path: "src/app/api/portal/payments/coming-soon/route.ts"
provides: "GET endpoint returning balance and payment instructions"
exports: ["GET"]
key_links:
- from: "src/lib/services/portal-ticket-service.ts"
to: "src/lib/services/ticket-service.ts"
via: "delegates ticket creation to existing createTicket"
pattern: "createTicket"
- from: "src/app/api/portal/tickets/route.ts"
to: "src/lib/services/portal-ticket-service.ts"
via: "createPortalTicket and listPortalTickets calls"
pattern: "(createPortalTicket|listPortalTickets)"
---
<objective>
Build portal ticket submission with conversation threads, and scaffold the online payment "coming soon" page with outstanding balance display.
Purpose: PORT-03 (ticket submission) and PORT-05 (online payment scaffold). Subscribers can interact with support through the portal.
Output: TicketComment model, portal ticket APIs, payment scaffold endpoint, 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
@.planning/phases/05-visibility-and-client-portal/05-02-SUMMARY.md
@src/lib/services/ticket-service.ts
@src/lib/services/portal-service.ts
@src/lib/casl/permissions.ts
@prisma/schema.prisma
</context>
<tasks>
<task type="auto">
<name>Task 1: TicketComment model and portal ticket service</name>
<files>prisma/schema.prisma, src/lib/services/portal-ticket-service.ts</files>
<action>
**Schema (prisma/schema.prisma):**
Add TicketComment model for conversation threads:
```prisma
model TicketComment {
id String @id @default(uuid())
tenantId String
ticketId String
ticket Ticket @relation(fields: [ticketId], references: [id])
/// Who posted this comment — can be subscriber (CLIENT) or staff
authorId String
author User @relation(fields: [authorId], references: [id])
/// For portal comments, link to the subscriber directly
subscriberId String?
message String
createdAt DateTime @default(now())
@@index([tenantId])
@@index([ticketId])
}
```
Add `comments TicketComment[]` relation to the Ticket model.
Add `ticketComments TicketComment[]` relation to the User model.
Run `npx prisma db push`.
**PortalTicketService (src/lib/services/portal-ticket-service.ts):**
1. **createPortalTicket(db, tenantId, subscriberId, data: { categoryId, subject, description })**
- Look up the subscriber to get their User record (if they have one via the portal auth) OR use the subscriberId for the createdById field
- Important: The subscriber may not have a User record. For portal ticket creation, we need a userId. Two approaches:
- Option A (preferred): When portal auth creates a session, the session has a userId. Use that.
- Option B: Create a shadow User record for the subscriber. Too complex.
- Actually, re-examine: the existing Ticket model requires `createdById -> User.id`. But portal subscribers authenticate via Subscriber, not User. Resolution: When a subscriber gets portal access (passwordHash set), also create a corresponding User record with Role.CLIENT. This should be done in plan 02's auth setup. If plan 02 already handles this, use the userId from the session. If not, this task must handle it.
- Simpler approach: Portal auth returns a user-like session. The `subscriberId` in the JWT IS the subscriber's ID. For ticket creation, use `createdById` as the session user's ID. But the session user for portal is mapped from Subscriber, not User table.
- **Resolution:** Create portal tickets using the existing `createTicket` from ticket-service.ts, but pass `source: 'PORTAL'` (TicketSource.PORTAL exists in the enum). The `createdById` needs a valid User.id. So portal subscribers MUST have a corresponding User record with CLIENT role. Add this to the portal auth flow: when a subscriber logs in for the first time, ensure a User record exists with their info and CLIENT role. Use `upsert` by subscriber email or a portal-specific identifier.
- Delegate actual ticket creation to existing `createTicket()` from ticket-service.ts with source=PORTAL
- Return the created ticket
2. **listPortalTickets(db, subscriberId, options?: { page, limit })**
- Query Ticket where subscriberId, ordered by createdAt DESC
- Include: category (name), latest comment
- Return paginated list with status
3. **getPortalTicket(db, subscriberId, ticketId)**
- Get single ticket with ALL comments (conversation thread)
- Verify ticket.subscriberId === subscriberId (security check)
- Include: category, all comments ordered by createdAt ASC, each comment with author name
- Return ticket with comments array
4. **addTicketComment(db, tenantId, subscriberId, ticketId, message)**
- Verify ticket exists AND ticket.subscriberId === subscriberId
- Verify ticket is not CLOSED (cannot comment on closed tickets)
- Create TicketComment with subscriberId set
- Return the created comment
**User record for portal subscribers:**
Add a helper `ensurePortalUser(db, tenantId, subscriber)` that:
- Checks if a User with email=subscriber.accountNumber (or a portal-specific convention) and tenantId exists
- If not, creates one with: email=`portal-{accountNumber}@portal.local`, roles=[CLIENT], firstName=subscriber.firstName, lastName=subscriber.lastName, passwordHash=subscriber.passwordHash
- Returns the User.id
- Call this during portal authentication (in auth-options.ts portal provider) or lazily on first ticket creation
</action>
<verify>`npx prisma db push` succeeds; `npx tsc --noEmit` compiles</verify>
<done>TicketComment model exists; PortalTicketService creates tickets via existing ticket-service, supports conversation threads</done>
</task>
<task type="auto">
<name>Task 2: Portal ticket API routes, payment scaffold, and tests</name>
<files>src/app/api/portal/tickets/route.ts, src/app/api/portal/tickets/[id]/route.ts, src/app/api/portal/tickets/[id]/comments/route.ts, src/app/api/portal/payments/coming-soon/route.ts, src/lib/__tests__/portal-ticket-service.test.ts</files>
<action>
**Portal Ticket API Routes:**
All routes use the `withPortalAuth` helper from plan 02 (or re-implement the same pattern: check session.subscriberId exists).
**GET /api/portal/tickets** — List subscriber's tickets (paginated)
**POST /api/portal/tickets** — Create a new ticket: accepts `{ categoryId, subject, description }`
**GET /api/portal/tickets/[id]** — Get ticket detail with conversation thread
**GET /api/portal/tickets/[id]/comments** — List comments for a ticket
**POST /api/portal/tickets/[id]/comments** — Add a comment: accepts `{ message }`
For [id] routes, use the dynamic route handler pattern from Phase 2+:
```typescript
export function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
// await params, then call service
}
```
**Payment Scaffold (src/app/api/portal/payments/coming-soon/route.ts):**
GET endpoint using withPortalAuth:
- Query subscriber's outstanding balance (sum of unpaid invoice amounts)
- Query tenant settings for payment instructions (add `paymentInstructions` text field to TenantSettings if not present — nullable String?)
- Return: `{ outstandingBalance: Decimal, paymentInstructions: string | null, message: "Online payments coming soon" }`
If TenantSettings needs a new field, add it to schema.prisma and run `npx prisma db push`.
**Integration Tests (src/lib/__tests__/portal-ticket-service.test.ts):**
Setup: Create tenant, create subscriber with passwordHash, ensure portal User record exists, seed ticket categories.
Tests (minimum 6):
1. `createPortalTicket creates ticket with PORTAL source` — verify ticket.source === 'PORTAL'
2. `createPortalTicket ticket appears in staff listTickets` — create via portal, query via staff service, verify it exists
3. `listPortalTickets returns only subscriber's tickets` — create tickets for 2 subscribers, verify isolation
4. `addTicketComment creates conversation entry` — add comment, verify it's linked to ticket
5. `addTicketComment rejects on closed ticket` — close ticket, try to comment, expect error
6. `getPortalTicket returns ticket with full conversation thread` — add 3 comments, verify all returned in order
Cleanup: ticketComments -> tickets -> ticketCategories -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> users -> tenant
</action>
<verify>`npx vitest run src/lib/__tests__/portal-ticket-service.test.ts` — all tests pass</verify>
<done>Subscribers can create tickets, view ticket list, have conversations on tickets; payment scaffold shows balance; 6+ tests pass; PORT-03 and PORT-05 satisfied</done>
</task>
</tasks>
<verification>
- `npx prisma db push` applies TicketComment model and any TenantSettings additions
- `npx tsc --noEmit` passes
- `npx vitest run src/lib/__tests__/portal-ticket-service.test.ts` — all tests pass
- Portal ticket appears in staff's existing ticket query (cross-system verification)
</verification>
<success_criteria>
- Subscriber creates ticket from portal with PORTAL source
- Ticket conversation thread supports subscriber and staff messages
- Closed tickets reject new comments
- Payment scaffold returns outstanding balance and instructions
- PORT-03, PORT-05 requirements satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/05-visibility-and-client-portal/05-03-SUMMARY.md`
</output>