Support System
The support system provides in-app ticket management for end users, a dedicated support agent portal, and super admin oversight. It covers ticket creation, messaging (with internal notes), FAQ management, agent assignment, dashboard analytics, and real-time WebSocket subscriptions.
Access Tiers
A user becomes a support agent when a super admin calls adminAssignSupportRole(userId). The SupportAgentGuard checks this at runtime. The User type exposes an isSupportAgent boolean field for the frontend to conditionally render the support portal.
support role UUIDSupportRoleService.onModuleInit (src/support/services/support-role.service.ts) upserts the global support role on every backend boot. The create branch pins the canonical UUID 1fd9c33f-52ec-4fc5-a9e9-102dcb4c6862 so any fresh database (new env, restored backup) lands on the same id deterministically. The update: {} clause on the upsert is intentional — pre-existing rows are not touched, so envs whose Role.id already drifted (for example, dev historically had 52007617-e2d6-45a8-83ec-5934d961a631) keep their current id. The support role flows through the UserRole global bridge, not UserCompanyRole, so it never participates in CompanyWriteGuard or per-company tier checks. Aligning a drifted env to the canonical id requires a manual SQL update against the Role table after first re-pointing any UserRole.roleId rows that reference the old id; only super admins can perform this against prod RDS.
Data Model
Ticket Lifecycle
Roles on a Ticket
A ticket can reference up to three distinct users:
| Field | Required | Set By | Purpose |
|---|---|---|---|
createdById | Yes | createSupportTicket (current user) | The reporter who filed the ticket |
assignedToId | No | assignTicket (agent) | The support agent handling triage and reply |
testerId | No | createSupportTicket (optional) or assignTester (agent) | The user responsible for verifying the reported bug is fixed or the requested feature behaves correctly |
The tester is intentionally separate from the assignee so that the person who works on the fix is not the same person who signs it off. Any user (not just support agents) can be set as the tester — the typical case is the original reporter, a developer, or a QA engineer. Setting testerId to null via assignTester clears the field. Tester changes are logged with action: TESTER_CHANGED.
After-Fix Status Transition ("Ready for Retest")
When a support ticket is fixed, deployed to prod, and handed to QA for verification, the status transition is always to TEST — never WAITING_ON_CUSTOMER, never RESOLVED. The UI column labeled "Ready for Retest" on the agent board is backed by backend status TEST; RESOLVED means the agent has signed off after retest passed.
mutation {
updateTicketStatus(ticketId: "<id>", status: "TEST") { id status }
assignTester(ticketId: "<id>", testerId: "<tester-user-id>") { id }
}
Both calls always go together: moving to TEST without a testerId leaves the ticket orphaned in the "Ready for Retest" column with no assignee to notify, and the TESTER_CHANGED event doesn't fire so no in-app/email notification is sent. See Email Notifications → Tester Assigned below for the notification payload.
Both halves of the old "replies vanish" problem were fixed on 2026-04-23:
- Backend:
sendSupportReplyno longer auto-transitions the ticket toWAITING_ON_CUSTOMER— agents must set that status explicitly viaupdateTicketStatuswhen they genuinely need the reporter to respond. - Frontend:
components/support/portal/TicketQueue.tsxremovedWAITING_ON_CUSTOMERfromDEFAULT_HIDDEN_STATUSES; a one-time localStorage migration (support-ticket-hidden-statuses-migration-v1) strips it from legacy persisted lists so existing agents see waiting tickets without toggling visibility. OnlyCLOSEDis hidden by default now.
Reserve WAITING_ON_CUSTOMER for the case where we genuinely need input from the reporter (clarification, repro steps) — not for our own fix/retest cycle.
Historical incident (2026-04-21): Shipped a 21-ticket batch end-to-end and moved 16 of them to WAITING_ON_CUSTOMER instead of TEST. The UI's "Ready for Retest" column showed 12 tickets instead of the ~28 expected. Fix was a bulk updateTicketStatus(status: "TEST") + assignTester over the affected IDs. Always target TEST for post-deploy handoff.
Auto-Transitions
| Trigger | From | To |
|---|---|---|
| Agent assigned to OPEN ticket | OPEN | IN_PROGRESS |
| User replies while waiting | WAITING_ON_CUSTOMER | IN_PROGRESS |
| User replies to resolved ticket | RESOLVED | OPEN |
| Cron job (hourly) | RESOLVED > 7 days | CLOSED |
Agent replies (sendSupportReply) do not auto-transition the status — that was removed on 2026-04-23 because the portal hid WAITING_ON_CUSTOMER and replied tickets dropped out of the replier's own queue.
Key Timestamps
| Field | Set When |
|---|---|
firstResponseAt | First agent reply (never overwritten) |
resolvedAt | Status changes to RESOLVED |
closedAt | Status changes to CLOSED |
lastActivityAt | Every status change, message, or assignment |
Enums
TicketCategory
| Value | Description |
|---|---|
GENERAL | General inquiry |
BILLING | Billing and payment |
TECHNICAL | Technical issue |
ACCOUNT | Account management |
FEATURE_REQUEST | Feature request |
BUG_REPORT | Bug report |
TicketPriority
LOW | MEDIUM (default) | HIGH | URGENT
TicketStatus
Dynamic — not a Prisma enum. SupportTicket.status is a plain string whose allowed values come from the TicketStatusConfig table and are administered via adminCreateTicketStatus / adminUpdateTicketStatus / adminDeleteTicketStatus mutations.
Default seed (applied on startup if the table is empty):
| name | label | color | sortOrder | flags |
|---|---|---|---|---|
OPEN | Open | #3B82F6 | 0 | isDefault, isActive |
IN_PROGRESS | In Progress | #F59E0B | 1 | isInProgress, isActive |
WAITING_ON_CUSTOMER | Waiting on Customer | #6B7280 | 2 | isWaiting, isActive |
TEST | Test | #ae00ff | 3 | isInProgress, isActive |
RESOLVED | Resolved | #10B981 | 5 | isResolved, isActive |
CLOSED | Closed | #EF4444 | 6 | isTerminal, isActive |
The name column is the machine identifier stored on tickets; the label column is the UI display string. Renaming via adminUpdateTicketStatus changes the label without touching ticket rows. The frontend reads the table at runtime (see Frontend Guide → Dynamic status rendering) — no hardcoded map, so an admin rename propagates without a redeploy.
TEST row was renamed from the legacy default READY_FOR_RETEST. If you're looking at older dev data or an old frontend bundle that still hardcodes READY_FOR_RETEST, those tickets will render as "unknown status" — the fix is to drop the hardcoded labels (done in PR #644) and let the UI read from TicketStatusConfig.TicketMessageSenderRole
USER | SUPPORT | SYSTEM
FaqStatus
DRAFT | PUBLISHED | ARCHIVED
TicketAuditAction
CREATED | STATUS_CHANGED | PRIORITY_CHANGED | CATEGORY_CHANGED | ASSIGNED | UNASSIGNED | REASSIGNED | TESTER_CHANGED | MESSAGE_SENT | INTERNAL_NOTE_ADDED | ATTACHMENT_ADDED | ATTACHMENT_REMOVED | DESCRIPTION_CHANGED | MESSAGE_EDITED | MESSAGE_DELETED | RESOLVED | CLOSED | REOPENED | DELETED | ARCHIVED | UNARCHIVED
Constants & Defaults
| Constant | Value |
|---|---|
| Ticket number format | SUP-00001 (5-digit zero-padded) |
| Default page size (tickets) | 12 |
| Default page size (audit log) | 30 |
| Default page size (FAQs) | 12 |
| Auto-close after RESOLVED | 7 days (hourly cron) |
| Presigned URL TTL | 15 minutes |
| Max attachments per message | 5 |
| Max attachment size | 10 MB |
File Attachments
Messages on tickets support file attachments using the platform's presigned URL upload pattern. Files are stored in S3 and download URLs are generated on-demand with 15-minute expiry.
Constraints
| Rule | Value |
|---|---|
| Max files per message | 5 |
| Max file size | 10 MB |
| Content or attachments | At least one required per message |
| Allowed types | Images, PDFs, Office docs, text, CSV, ZIP |
Attachments are supported on sendTicketMessage, sendSupportReply, and addInternalNote. The content field is optional when attachments are provided.
When creating a ticket with files, the frontend creates the ticket first, then sends an initial message with the attachments.
Removing Attachments
Once a message has been sent, individual attachments can still be removed via the removeTicketAttachment(attachmentId) mutation. This addresses the common case where a user accidentally uploads the wrong file and only notices after the message has been delivered.
| Rule | Value |
|---|---|
| Who can remove | Ticket creator, support agent, or super admin (server-side check) |
| Status restriction | None — allowed even on CLOSED tickets |
| Time window | None — no expiry on the removal window |
| Empty-message cascade | If removing the last attachment leaves the message with no text, the parent TicketMessage is deleted in the same transaction (messageDeleted: true) |
| S3 cleanup | DB-first inside the transaction; StorageService.deleteFile runs best-effort afterwards. Failures are logged but never rolled back — orphan blobs are recoverable, while a missing blob behind a live row is a permanently broken download link |
| Audit | A TicketAuditLog row with action: ATTACHMENT_REMOVED and metadata: { attachmentId, messageId, filename, messageDeleted } |
| Real-time | Publishes supportTicketEvent (NEW_MESSAGE) to agents and myTicketUpdated (NEW_MESSAGE) to the ticket creator so live ticket-detail subscribers refetch |
Real-Time Subscriptions
Two WebSocket subscriptions power the real-time updates:
| Subscription | Guard | Channel | Purpose |
|---|---|---|---|
myTicketUpdated(ticketId?) | JwtAuth | myTicketUpdated:{userId} | Notify ticket creator of replies, status changes |
supportTicketEvent | JwtAuth + SupportAgent | supportTicketEvent | Notify all agents of new tickets, assignments, status changes |
Event Types
| TicketEventType | Trigger |
|---|---|
NEW_TICKET | User creates a ticket |
NEW_MESSAGE | Agent replies or user sends message |
STATUS_CHANGED | Any status transition |
ASSIGNED | Ticket assigned or reassigned |
TESTER_CHANGED | Tester set, changed, or cleared via assignTester |
PRIORITY_CHANGED | Priority updated |
DESCRIPTION_CHANGED | Ticket description edited |
Email Notifications
Support events trigger branded in-app + email notifications via SupportNotificationListener. Emails go through generateEmailTemplate for consistent branding and are queued on the Bull mail queue for reliable delivery. All recipient queries filter isActive: true so deactivated accounts never receive notifications. Every outbound URL is built by buildAbsoluteUrl() → getBaseUrl() (reads SERVER_LIVE_PROTOCOL + SERVER_LIVE_DOMAIN) so in-app links and email links always point at the same host.
Recipient matrix at a glance
| Event | In-app | Notes | |
|---|---|---|---|
| Ticket created | creator | creator | Confirmation receipt only — no fan-out to super admins |
| Support reply (agent replies) | creator | creator | |
| Ticket assigned | new assignee + previous (on reassign) | new assignee only | Previous assignee gets in-app, not email |
| Tester changed | new tester | new tester | Email skipped when tester == assignee or no email on file |
| Status changed | creator + assignee (−actor) | creator only (−actor) | Assignees rely on the portal dashboard for status updates |
| Priority → URGENT | creator + assignee (−actor) | creator + assignee (−actor) | Fires only on non-URGENT → URGENT transitions |
| User messages | assignee, else silent | same | No fan-out to super admins when the ticket is unassigned |
| Category changed | creator + assignee (−actor) | same | |
| Description edited | creator + assignee (−actor) | same | Content NOT included in the notification body |
| Internal note | assignee + tester (−author) | same | Creator is never notified — internal notes are agent-only |
Every notification type is gated by the user's notifyOnSupportTickets preference. Listener handlers are fire-and-forget (try/catch wrapped, never rethrow) so a queueing failure for one recipient never breaks the mutation or the other recipients.
Ticket Created
When a user creates a support ticket, the ticket creator receives a creation confirmation:
| Channel | Content |
|---|---|
| In-app | Title Ticket received: SUP-XXXXX, link to /support/{id} (user-facing page) |
| Branded "We've got your ticket SUP-XXXXX" with ticket number, subject, category, priority badge, and a "View Ticket" button |
Historical context: this event previously fanned out to every super admin, which was too noisy in practice. The fan-out was removed — agents now discover new tickets via the supportTicketEvent real-time subscription (the portal dashboard), and the first agent is notified via email only when they get assigned (see below). The TICKET_CREATED event + SUPPORT_TICKET_CREATED enum are kept so a future triage-routing or analytics listener can wire a different handler without re-introducing the event.
Priority Badge Colors
| Priority | Background | Text |
|---|---|---|
URGENT | #dc3545 (red) | White |
HIGH | #fd7e14 (orange) | White |
MEDIUM | #ffc107 (yellow) | Dark |
LOW | #28a745 (green) | Dark |
Support Reply
When a support agent replies to a ticket, the ticket creator receives:
| Channel | Content |
|---|---|
| In-app | Plain-text reply snippet (HTML stripped, truncated to 100 chars), link to /support/{id} |
| Branded template with agent name, ticket subject, and rendered reply HTML (sanitized against the allow-list, truncated to 500 visible characters) |
Ticket Assigned
When a super admin or agent calls assignTicket(ticketId, agentId), the newly-assigned agent receives:
| Channel | Content |
|---|---|
| In-app | Title Ticket assigned to you: SUP-XXXXX, type SUPPORT_TICKET_ASSIGNED, priority URGENT if the ticket itself is URGENT else NORMAL |
| Branded template including priority + category badges, "Assigned by" actor name, link to the support-portal detail page |
On reassignment (previous assignee was non-null and differs from the new one), the previous assignee also receives an in-app notification (SUPPORT_TICKET_REASSIGNED) — no email. Self-assignment (actor == new assignee) and assigning to someone who already is the assignee are both no-ops.
Tester Assigned
When assignTester(ticketId, testerId) is called, the newly-assigned tester receives:
| Channel | Content |
|---|---|
| In-app | Title "You have been set as tester on SUP-XXXXX", type SUPPORT_TICKET_TESTER_ASSIGNED, link to the ticket detail page |
Branded template: subject You have been set as tester on SUP-XXXXX — <ticket subject>, with category/priority badges and a "View Ticket" button |
Dedupe rules — the handler skips the email (but still creates the in-app notification) when:
- The tester has no email on file (
newTesterEmailis null). - The tester is also the current assignee (
assignedToId === newTesterId). The assignee email fromassignTicketalready covered this user. - The tester is the actor who triggered the change (self-assignment).
Clearing the tester (assignTester(ticketId, testerId: null)) emits the event but the listener returns early because newTesterId is null — no notification, no email.
Status Changed
When status transitions (via updateTicketStatus, bulkUpdateTicketStatus, closeMyTicket, reopenMyTicket, or the hourly auto-close cron), the listener notifies the ticket creator + assignee, deduped, excluding whoever made the change:
| Channel | Content |
|---|---|
| In-app | Ticket status updated: SUP-XXXXX, message SUP-XXXXX moved from {oldLabel} to {newLabel}. URL is /support/{id} for the creator, /support-portal/tickets/{id} for agents so neither audience 404s. |
| Creator only (and only if creator is not the actor). Assignees see status changes in the portal dashboard instead of their inbox. |
The hourly auto-close cron emits a TICKET_STATUS_CHANGED per closed ticket with actorName = "System" and actorId = <system super admin>, so the email the creator receives reads "moved from RESOLVED to CLOSED by System".
Priority Escalated (→ URGENT)
updateTicketPriority emits TICKET_PRIORITY_URGENT only when the new priority is URGENT and the old priority was something else. Routine LOW↔MEDIUM↔HIGH re-triage stays silent so stakeholders aren't pinged every time a ticket is sorted.
| Channel | Content |
|---|---|
| In-app | Title SUP-XXXXX escalated to URGENT, priority URGENT, type SUPPORT_TICKET_PRIORITY_ESCALATED |
🚨 SUP-XXXXX escalated to URGENT — <subject> — red-accent template showing old → new priority and the actor who escalated |
Recipients: creator + assignee, deduped, minus the actor.
User Message (user → agent)
When a ticket user sends a message via sendTicketMessage, the event only notifies the current assignee:
| Channel | Content |
|---|---|
| In-app | Title New message from {senderName} on SUP-XXXXX, message SUP-XXXXX — {subject}: "{200-char preview}" |
New message on SUP-XXXXX — <subject> — branded template with sender name and a sanitized content preview |
If the ticket has no assignee, the handler stays silent — agents discover unassigned tickets through the supportTicketEvent PubSub stream in the support portal, and we explicitly chose not to fan out to super admins on user messages (same rule as TICKET_CREATED).
Category Changed
When an agent calls updateTicketCategory(ticketId, category) and the value actually changes (no-op reposts are skipped), creator + assignee (minus actor) receive:
| Channel | Content |
|---|---|
| In-app | Category changed on SUP-XXXXX, message SUP-XXXXX — {oldLabel} → {newLabel}, type SUPPORT_TICKET_CATEGORY_CHANGED |
Category updated on SUP-XXXXX — <subject> — shows old → new category and the actor name |
Description Edited
updateTicketDescription (agent-side) and updateMyTicketDescription (user-side) both emit TICKET_DESCRIPTION_CHANGED. No-op reposts are skipped. Creator + assignee (minus actor) receive:
| Channel | Content |
|---|---|
| In-app | Description edited on SUP-XXXXX, message {actorName} edited the description of SUP-XXXXX, type SUPPORT_TICKET_DESCRIPTION_CHANGED |
Description edited on SUP-XXXXX — <subject> — "open the ticket to see the updated content" |
The author may have been editing to redact something. Including the new content in the notification would leak what they were trying to clean up, so the notification only nudges the reader to open the ticket.
Internal Note
addInternalNote(ticketId, content) writes a ticket message with isInternalNote: true and fires TICKET_INTERNAL_NOTE. The handler notifies the assignee + tester, deduped, minus the author:
| Channel | Content |
|---|---|
| In-app | Internal note on SUP-XXXXX by {authorName}, message includes a 200-char plain-text preview, type SUPPORT_TICKET_INTERNAL_NOTE |
Internal note on SUP-XXXXX — <subject> — amber-accent template marking the note as agent-only, with a sanitized preview |
Internal notes are agent-to-agent by design. The ticket creator must never receive one — excluding them is an explicit branch in the handler, not an accident of the recipient math.
Bulk operations
bulkAssignTickets and bulkUpdateTicketStatus emit the same per-ticket events as their single-item counterparts inside the loop — TICKET_ASSIGNED per assignment (plus TICKET_STATUS_CHANGED when the auto-transition from the default status fires), TICKET_STATUS_CHANGED per status move. actorName is resolved once per batch so we don't do N user lookups for a 50-ticket bulk.
Previously bulk mutations updated the DB + audit log but emitted nothing, so agents bulk-assigned to tickets and creators of bulk-closed tickets were never notified. That's fixed: per-item emits reuse the existing handlers' dedup rules so they're safe at any batch size.
Event Payloads
Every payload carries an "emit-time snapshot" — the listener trusts the payload fields verbatim and never re-queries the DB for the same row, which keeps the handler deterministic even if another agent mutates the ticket between emit and consume.
| Event | Payload fields |
|---|---|
TICKET_CREATED | ticketId, ticketNumber, subject, description, category, priority, createdById, createdByName, createdByEmail |
TICKET_SUPPORT_REPLY | ticketId, ticketNumber, subject, createdById, replyContent, repliedById, repliedByName |
TICKET_ASSIGNED | ticketId, ticketNumber, subject, priority, category, newAssignedToId, newAssigneeEmail, newAssigneeFirstName, previousAssignedToId, isReassignment, ticketCreatorId, actorId, actorName |
TICKET_TESTER_CHANGED | ticketId, ticketNumber, subject, priority, category, newTesterId, newTesterEmail, newTesterFirstName, previousTesterId, ticketCreatorId, assignedToId, actorId, actorName |
TICKET_STATUS_CHANGED | ticketId, ticketNumber, subject, priority, category, oldStatus, newStatus, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, actorId, actorName |
TICKET_PRIORITY_URGENT | ticketId, ticketNumber, subject, category, oldPriority, newPriority, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName |
TICKET_MESSAGE_SENT | ticketId, ticketNumber, subject, priority, category, senderId, senderName, contentPreview, assignedToId |
TICKET_CATEGORY_CHANGED | ticketId, ticketNumber, subject, priority, oldCategory, newCategory, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName |
TICKET_DESCRIPTION_CHANGED | ticketId, ticketNumber, subject, priority, category, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName |
TICKET_INTERNAL_NOTE | ticketId, ticketNumber, subject, priority, authorId, authorName, contentPreview, assignedToId, assigneeEmail, assigneeFirstName, testerId, testerEmail, testerFirstName |
NotificationType enum
The Prisma NotificationType enum carries one value per wired handler. All are mapped to the notifyOnSupportTickets preference in notification-preference.service.ts so users can opt out of the whole category with a single toggle:
SUPPORT_TICKET_CREATED, SUPPORT_TICKET_REPLY, SUPPORT_TICKET_ASSIGNED, SUPPORT_TICKET_REASSIGNED, SUPPORT_TICKET_STATUS_CHANGED, SUPPORT_TICKET_TESTER_ASSIGNED, SUPPORT_TICKET_PRIORITY_ESCALATED, SUPPORT_TICKET_USER_MESSAGE, SUPPORT_TICKET_CATEGORY_CHANGED, SUPPORT_TICKET_DESCRIPTION_CHANGED, SUPPORT_TICKET_INTERNAL_NOTE.
Security
User-provided content is sanitized in two different ways depending on whether the field is plain text or rich-text HTML:
| Field | Sanitizer | Why |
|---|---|---|
ticketNumber, subject, createdByName, createdByEmail, repliedByName | validator.escape() | Plain text — escape <, >, &, ", ' so any HTML metachars render as literal text |
description, replyContent (email body) | sanitizeRichTextForEmail() | Rich text from the ticket form / TipTap — runs through sanitize-html against an allow-list of safe formatting tags so paragraphs, bold, lists, and links render but <script>, <img>, event handlers, etc. are stripped |
replyContent (in-app snippet) | htmlToPlainText() | The bell preview does not render HTML, so all tags are stripped before truncating to 100 chars |
The sanitizeRichTextForEmail() allow-list is: p, br, strong, b, em, i, u, s, ul, ol, li, blockquote, code, pre, h1–h4, a, span. Anchor tags get target="_blank" rel="noopener noreferrer" injected automatically; only http, https, and mailto URL schemes are allowed.
Truncation is done by visible character count (not raw HTML length) so half-open tags are never emitted. When content exceeds the 500-char limit, the sanitizer falls back to a plain-text + ellipsis paragraph.
Both helpers live in src/common/mail/sanitize-rich-text.ts and are reusable for any future email listener that needs to embed user-supplied rich text.
Implementation Files
| File | Purpose |
|---|---|
src/notification-system/listeners/support-notification.listener.ts | Event handlers, branded email composition |
src/common/mail/sanitize-rich-text.ts | sanitizeRichTextForEmail() and htmlToPlainText() helpers (shared, reusable) |
src/support/resolvers/user-support.resolver.ts | Emits TICKET_CREATED with enriched payload |
src/support/resolvers/support-portal.resolver.ts | Emits TICKET_SUPPORT_REPLY with enriched payload |
Cron Job
The TicketAutoCloseJob runs every hour at minute 0. It finds all tickets in RESOLVED status where resolvedAt is older than 7 days, then for each:
- Updates status to
CLOSED, setsclosedAt - Writes an audit log entry with
reason: auto_close_7_days - Appends a system message: "This ticket was automatically closed after 7 days of inactivity"
- Emits a per-ticket
TICKET_STATUS_CHANGEDevent withactorName = "System"soSupportNotificationListener.handleTicketStatusChangeddelivers the standard status-change notification to creator + assignee — the creator gets the email ("moved from RESOLVED to CLOSED by System"), the assignee gets the in-app notification. - Publishes a single batched
supportTicketEventto the agent dashboard so it refreshes once per cron sweep, not once per ticket.
The legacy TICKET_AUTO_CLOSED domain event was removed — it was emitted but never handled, so auto-closes used to go out silently. Per-ticket TICKET_STATUS_CHANGED gives everyone involved a real notification with zero duplication.
Backend Implementation Files
| File | Purpose |
|---|---|
src/support/support.module.ts | Module definition |
src/support/services/support-ticket.service.ts | Ticket CRUD, assignment, stats, auto-close |
src/support/services/ticket-message.service.ts | User messages, agent replies, internal notes |
src/support/services/ticket-audit.service.ts | Audit log creation |
src/support/services/faq.service.ts | FAQ CRUD, voting, suggestion generation |
src/support/services/support-role.service.ts | Role management (auto-upserts on startup) |
src/support/resolvers/user-support.resolver.ts | User-facing queries and mutations |
src/support/resolvers/support-portal.resolver.ts | Agent portal queries and mutations |
src/support/resolvers/admin-support.resolver.ts | Super admin queries and mutations |
src/support/resolvers/faq.resolver.ts | FAQ queries and mutations |
src/support/resolvers/ticket-attachment.resolver.ts | downloadUrl field resolver (S3 presigned) |
src/support/resolvers/support-subscription.resolver.ts | WebSocket subscriptions |
src/support/resolvers/user-support-agent-field.resolver.ts | User.isSupportAgent field resolver |
src/support/guards/support-agent.guard.ts | SupportAgentGuard (checks role or superAdmin) |
src/support/jobs/ticket-auto-close.job.ts | Hourly auto-close cron |
src/notification-system/listeners/support-notification.listener.ts | Email notifications for ticket events |
src/support/constants/support.constants.ts | PubSub channels, events, defaults |
src/support/dto/support-enums.ts | Enum registrations |
src/support/dto/support-ticket.input.ts | Input types |
src/support/dto/faq.input.ts | FAQ input types |
src/support/dto/support-ticket.output.ts | Output types |
Related
- Support Frontend Guide — Complete GraphQL operations, page structure, and component breakdown for frontend implementation