Help Desk — Developer Guide
Simplified mirror of Expenses / Tasks (numbering, status machine, assignee scoping, notes, domain timeline) — no hard module dependencies. contact_id and company_id are both nullable soft links, validated only when the corresponding module is entitled.
Backend layout
| Piece | Path |
|---|---|
| Models | app/Models/HelpDeskTicket.php, HelpDeskCategory, HelpDeskSlaPolicy, HelpDeskMailbox, HelpDeskNote, HelpDeskActivity, HelpDeskTicketAttachment |
| Enums | HelpDeskStatusEnum, HelpDeskPriorityEnum, HelpDeskActivityTypeEnum (includes sla_applied, sla_response_met, sla_breached) |
| Service | HelpDeskTicketService (+ ScopesToAssignee, RetriesOnDuplicateNumber), HelpDeskSlaClockService, HelpDeskSlaPolicyService, HelpDeskMailboxService, HelpDeskMailIngestService, HelpDeskCategoryService, HelpDeskCategorySeederService |
| Controller | HelpDeskTicketController, HelpDeskCategoryController, HelpDeskSlaPolicyController, HelpDeskMailboxController |
| Requests | app/Http/Requests/Tenant/Api/V1/HelpDesk/*, HelpDeskCategory/*, HelpDeskSlaPolicy/*, HelpDeskMailbox/* |
| Resources | app/Http/Resources/Tenant/Api/V1/HelpDesk/*, HelpDeskSlaPolicy/*, HelpDeskMailbox/* |
| Policy | HelpDeskTicketPolicy, HelpDeskCategoryPolicy, HelpDeskSlaPolicyPolicy, HelpDeskMailboxPolicy (maps to help-desk.*) |
| Events | app/Events/HelpDeskTicket*.php, HelpDeskSlaBreached |
| Subscriber | app/Listeners/HelpDeskEventSubscriber.php (audit + assignment/status/SLA notifications) |
| Notifications | HelpDeskAssignedNotification, HelpDeskStatusNotification, HelpDeskSlaBreachNotification |
| Automation | Wired triggers help_desk.ticket_created, help_desk.ticket_status_changed, help_desk.sla_breached via AutomationTriggerRegistry + AutomationEventBridge |
| Commands / jobs | help-desk:scan-sla-breaches (every 5 min); help-desk:sync-mailboxes (every minute) → SyncHelpDeskMailboxJob on queue help-desk-ingest |
| Link rules | LinkableContact, LinkableCompany, LinkableKnowledgeBaseArticle — optional, tenant-scoped, module-entitlement-checked |
| Pivot | help_desk_ticket_knowledge_base_article — soft M2M (no module_dependencies row) |
| Tests | HelpDeskTicketTest, HelpDeskCategoryTest, HelpDeskKnowledgeBaseLinkTest, HelpDeskSlaTest, HelpDeskMailIngestTest |
| Migrations | 2026_08_14_* baseline … 2026_08_30_16000* SLA (1.3.0) … 2026_08_30_11530* mailboxes + email source (1.4.0) |
Domain notes
- No hard module dependency: Help Desk has no
module_dependenciesrow — installable standalone.contact_id/company_idare nullable columns. - Tenant categories:
help_desk_categorieslookup (name, slug,sort_order,is_active, soft deletes).help_desk_tickets.category_idis a nullable FK. Category CRUD reuseshelp-desk.view|create|update|delete|restore|force.delete— nohelp-desk-categories.*family.HelpDeskCategorySeederService::ensureDefaults()lazily inserts General / Technical / Billing / Account / Other (slugsgeneral|technical|billing|account|other) on first list/create; lazy seed does not write activity. Starter slugs are immutable on update. Other cannot be soft- or force-deleted (422). Listing does not restore deleted starters except a missing/trashed Other. Delete/forceDelete blocked while any tickets (including trashed, for force) still reference the category. Spatie log namehelp-desk-categories. - Status machine on
HelpDeskStatusEnum::allowedTransitions()/canTransitionTo():open → in_progress|waiting|resolved|closed,in_progress → waiting|resolved|closed,waiting → in_progress|resolved|closed,resolved → closed|open,closed → open.HelpDeskTicketService::transitionStatus()throwsValidationException(422,statusfield) for disallowed transitions. - Content updates (
PUT) blocked whenstatus === closedviaHelpDeskTicket::isEditable(). Assignment after submit usesPOST …/assign. close()/reopen()are assignee-scoped inHelpDeskTicketPolicy(same asview/update) unless the actor hashelp-desk.assignor is superadmin.contact_id/company_idvalidated viaLinkableContact/LinkableCompany— null always passes; non-null requires module entitlement, tenant scope, and assignee rules when applicable.knowledge_base_article_idson create/update andPUT …/articlessync viaHelpDeskTicketService::syncKnowledgeBaseArticles()— requiresmodule:knowledge-base;LinkableKnowledgeBaseArticleallows published for view-only actors or any visible article when actor hasknowledge-base.update. Recordsarticles_syncedon the domain timeline when the set changes.help-desk.force.deleteis not granted to any default role — owner/superadmin only.- Auto-numbering:
HelpDeskTicketService::nextNumber()readshelp_desk_number_prefixtenant setting (defaultHD-), zero-pads running count to 5 digits. Exposed viaPUT /settings(UpdateTenantSettingsRequest).unique(tenant_id, number)DB index;create()retries up to 3 times viaRetriesOnDuplicateNumber. - Overdue scope: open statuses (
open,in_progress,waiting) withdue_at < UtcInstant::now()— aligns with workspace timezone KPI fixes elsewhere. - SLA clocks (
HelpDeskSlaClockService): most-specific active policy (category+priority > priority > category > default);UtcDateTimecolumns; first response on first staff note or leave-opentransition;help-desk:scan-sla-breachesmarks response/resolve breaches viaUtcInstant. - Email intake: tenant-scoped
help_desk_mailboxes(encrypted IMAP password); ingest reusesMailboxClient/MailboxConnection(not personalEmailAccount); replies matched by ticket number prefix; idempotentsource_message_id. - Dashboard widget:
DashboardWidgetServiceregistershelp_desk_my_opengated bymodule:help-desk+help-desk.view.
Permissions
help-desk.view | create | update | delete | restore | force.delete | assign | close | reopenRoutes use module:help-desk then can:help-desk.* / policies. SLA policy and mailbox CRUD reuse the same permissions (categories pattern).
Catalog: slug help-desk, category operations, is_default_included = false, is_billable = false, sort_order = 10, version 1.10.0. Registered via DefaultModuleRegistrar migration (migrate-only) — no module_dependencies row.
Communication Templates (soft)
HelpDeskPlaceholderProvider registers context help-desk on PlaceholderRegistry (see Communication Templates). Ticket placeholders include number, subject, status, priority, category_name, name / email / phone (from linked contact), company_name, assignee_name, plus shared agent/workspace/system tokens. WhatsApp phone resolves from the soft-linked contact — tickets without a contact phone cannot render WhatsApp extras.
SPA: ticket view soft-gates the shared WhatsAppTemplatePickerDialog when module:communication-templates + communication-templates.use and the linked contact has a phone. No module_dependencies row between Help Desk and Communication Templates.
API (tenant)
Base: /api/tenant/v1 — full reference tenant-v1-help-desk.md.
Frontend
SPA mirrors Expenses (dedicated create/view/edit pages, no create/edit page or record page) under AppLayout.
| Piece | Path |
|---|---|
| Page | src/pages/help-desk/ (help-desk-page.tsx, help-desk-form.tsx, help-desk-form-page.tsx, help-desk-view-page.tsx, help-desk-categories-dialog.tsx, help-desk-sla-policies-dialog.tsx, help-desk-mailboxes-dialog.tsx) |
| Shared board | src/components/crm/kanban-board.tsx (status Kanban; per-column vertical scroll + contained horizontal scroll; titles stay fixed) |
| View page | Details (category, priority, status, due date, SLA clocks, source, assignee, related contact/company + soft-gated WhatsApp template picker, related KB articles), notes with @mentions, timeline — actions: assign, add note, status transitions, close, reopen, edit (non-closed), delete |
| Form page | Subject, description, category picker, priority, due date, conditional contact/company pickers, and Knowledge base articles multi-select when hasModule('knowledge-base') + knowledge-base.view |
| Service | helpDeskService + helpDeskCategoryService + helpDeskSlaPolicyService + helpDeskMailboxService in src/api/services.ts |
| Types | HelpDesk* in src/types/api.ts |
| Query keys | QUERY_KEYS.helpDeskTickets / helpDeskTicket(id) / helpDeskTicketTimeline(id) / helpDeskStats / helpDeskCategories / helpDeskSlaPolicies / helpDeskMailboxes |
| Permissions | PERMISSIONS.helpDesk.* |
| Nav | Operations sidebar group — permission: PERMISSIONS.helpDesk.view, module: 'help-desk' |
| Route | tenantRoutes.helpDesk = '/help-desk', lazy-loaded in App.tsx behind RequireAccess module="help-desk" |
| Dashboard | tenant-dashboard-widgets.tsx — help_desk_my_open widget |
| Notifications | src/notifications/modules/help-desk.ts — assigned/closed/reopened/due/overdue/SLA breach types (deep link /help-desk/:id) |
| Playwright | e2e/pages/help-desk.page.ts, e2e/tests/help-desk/, npm run test:e2e:help-desk |
Tests
php artisan test --compact tests/Feature/Tenant/HelpDesk/
npm run typecheck && npm run lint && npm run build
npm run test:e2e:help-deskLogging
- Spatie
LogsActivityonHelpDeskTicket(log namehelp-desk) - Domain
help_desk_activitiestimeline PlatformAuditServiceviaHelpDeskEventSubscriber
Distinct from Central Feedback
| Central Feedback | Help Desk |
|---|---|
Platform concern — no module:* gate | Licensed module:help-desk |
| Tenant submit → Central triage | Tenant-scoped internal queue |
feedback.* permissions (Central) | help-desk.* permissions (Tenant) |
| Product bug/feature intake | Workspace support / ops tickets |
| Give Feedback shell dialog | File a complaint shell dialog (ComplaintDialog) → POST /help-desk |
Tenant SPA mounts both dialogs from the app shell. File a complaint is gated by module:help-desk + help-desk.create and reuses existing ticket create APIs — no parallel complaint tables.
AI tools
Ask EloSync Help Desk tools (get_help_desk_ticket, confirmed status/assign/note writes) are registered in AIToolRegistry and confirmed via PendingAiActionService. See AI tools and AI Help Desk triage production readiness.
Deferred
- Customer portal, chat/social intake, Kanban