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>
11 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 | 03 | execute | 2 |
|
|
true |
|
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.
<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 @.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
Task 1: TicketComment model and portal ticket service prisma/schema.prisma, src/lib/services/portal-ticket-service.ts **Schema (prisma/schema.prisma):**Add TicketComment model for conversation threads:
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):
-
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
subscriberIdin the JWT IS the subscriber's ID. For ticket creation, usecreatedByIdas 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
createTicketfrom ticket-service.ts, but passsource: 'PORTAL'(TicketSource.PORTAL exists in the enum). ThecreatedByIdneeds 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. Useupsertby 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
-
listPortalTickets(db, subscriberId, options?: { page, limit }) —
- Query Ticket where subscriberId, ordered by createdAt DESC
- Include: category (name), latest comment
- Return paginated list with status
-
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
-
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
npx prisma db pushsucceeds;npx tsc --noEmitcompiles TicketComment model exists; PortalTicketService creates tickets via existing ticket-service, supports conversation threads
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+:
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
paymentInstructionstext 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):
createPortalTicket creates ticket with PORTAL source— verify ticket.source === 'PORTAL'createPortalTicket ticket appears in staff listTickets— create via portal, query via staff service, verify it existslistPortalTickets returns only subscriber's tickets— create tickets for 2 subscribers, verify isolationaddTicketComment creates conversation entry— add comment, verify it's linked to ticketaddTicketComment rejects on closed ticket— close ticket, try to comment, expect errorgetPortalTicket 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
npx vitest run src/lib/__tests__/portal-ticket-service.test.ts — all tests pass
Subscribers can create tickets, view ticket list, have conversations on tickets; payment scaffold shows balance; 6+ tests pass; PORT-03 and PORT-05 satisfied
<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>