--- phase: 05-visibility-and-client-portal plan: 03 subsystem: portal tags: [portal, tickets, comments, conversation, payment-scaffold, subscriber] # Dependency graph requires: - phase: 03-operational-modules provides: Ticket model, TicketService with createTicket and status transitions - phase: 05-visibility-and-client-portal plan: 02 provides: Portal auth (withPortalAuth, subscriberId in session), PortalService provides: - TicketComment model for conversation threads - Portal ticket service (createPortalTicket, listPortalTickets, getPortalTicket, addTicketComment) - ensurePortalUser shadow User pattern for subscriber->User FK bridge - Portal ticket API routes (list, create, detail, comments) - Payment scaffold endpoint with outstanding balance and instructions affects: [05-04 (network monitoring may reference tickets), 05-05 (e2e tests)] # Tech tracking tech-stack: added: [] patterns: - "ensurePortalUser shadow User for subscriber->User FK bridge" - "TicketComment append-only conversation thread on tickets" - "Payment scaffold returning balance + instructions (gateway deferred to v2)" key-files: created: - prisma/schema.prisma (TicketComment model, paymentInstructions field) - 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 modified: - prisma/schema.prisma key-decisions: - "ensurePortalUser creates shadow User with CLIENT role and portal-{accountNumber}@portal.local email -- bridges Subscriber auth to User FK on Ticket.createdById" - "Portal tickets use source=SUBSCRIBER (not PORTAL) -- TicketSource enum has STAFF/SUBSCRIBER, SUBSCRIBER is semantically correct" - "TicketComment is append-only (no edits/deletes) -- conversation integrity preserved" - "Closed tickets reject new comments -- enforced at service layer" - "Payment scaffold computes outstanding balance in JS from SENT/PARTIAL/OVERDUE invoices -- same pattern as other derived aggregations" patterns-established: - "Shadow User pattern: portal subscribers get lazy-created User records for FK constraints" - "Ticket conversation threads via TicketComment with chronological ordering" # Metrics duration: 5min completed: 2026-03-05 --- # Phase 05 Plan 03: Portal Ticket Submission and Payment Scaffold Summary **Subscriber portal ticket creation with conversation threads, shadow User FK bridge, and payment coming-soon endpoint with outstanding balance -- 6 tests passing** ## Performance - **Duration:** 5 min - **Started:** 2026-03-05T09:30:50Z - **Completed:** 2026-03-05T09:35:36Z - **Tasks:** 2 - **Files modified:** 8 ## Accomplishments - Subscribers can create support tickets from the portal (source=SUBSCRIBER) - Conversation threads on tickets via TicketComment model (append-only, chronological) - Shadow User pattern bridges Subscriber auth to User FK on Ticket.createdById - Portal tickets visible in staff ticket queue (cross-system verified) - Closed tickets reject new comments - Payment scaffold returns outstanding balance and configurable payment instructions ## Task Commits Each task was committed atomically: 1. **Task 1: TicketComment model and portal ticket service** - `2d5b9ca` (feat) 2. **Task 2: Portal ticket API routes, payment scaffold, and tests** - `3f44f5c` (feat) ## Files Created/Modified - `prisma/schema.prisma` - Added TicketComment model, paymentInstructions on TenantSettings, ticketComments relation on User - `src/lib/services/portal-ticket-service.ts` - Portal ticket service with ensurePortalUser, createPortalTicket, listPortalTickets, getPortalTicket, addTicketComment - `src/app/api/portal/tickets/route.ts` - GET (list) and POST (create) for portal tickets - `src/app/api/portal/tickets/[id]/route.ts` - GET ticket detail with conversation thread - `src/app/api/portal/tickets/[id]/comments/route.ts` - GET (list) and POST (add) ticket comments - `src/app/api/portal/payments/coming-soon/route.ts` - GET outstanding balance and payment instructions - `src/lib/__tests__/portal-ticket-service.test.ts` - 6 integration tests ## Decisions Made - **Shadow User pattern:** Portal subscribers authenticate as Subscriber (not User), but Ticket.createdById requires User.id. ensurePortalUser() lazily creates a User with CLIENT role and email `portal-{accountNumber}@portal.local` to bridge this gap. - **SUBSCRIBER source (not PORTAL):** The TicketSource enum has STAFF and SUBSCRIBER. Used SUBSCRIBER which is semantically correct for portal-submitted tickets. - **Append-only comments:** TicketComment has no updatedAt -- comments cannot be edited or deleted, preserving conversation integrity. - **Closed ticket comment rejection:** Enforced at service layer -- addTicketComment checks ticket.status !== CLOSED before creating comment. - **Outstanding balance in JS:** Payment scaffold computes balance from SENT/PARTIAL/OVERDUE invoices in JavaScript, consistent with the derived-aggregation pattern used throughout the project. ## Deviations from Plan None -- plan executed exactly as written. ## Issues Encountered None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - PORT-03 (ticket submission) and PORT-05 (online payment scaffold) requirements satisfied - Portal now has: auth, account view, invoices, payments, tickets, and payment scaffold - Ready for 05-04 (network monitoring) and 05-05 (e2e tests) --- *Phase: 05-visibility-and-client-portal* *Completed: 2026-03-05*