Skip to content

Ticket Types For New Tickets Developer Guide

Ed Mozley edited this page Nov 2, 2026 · 1 revision

🛠️ A type for every new ticket - Developer Guide

The design behind A type for every new ticket (#230, 4.8.0): a default type and ordered rules, applied before a ticket is inserted, on every path that creates one.


1. 📁 Files

File What it does
includes/ticket_type_rules.php Everything that decides: TICKET_TYPE_CHANNELS, ticketTypeRuleConditions() (cleans criteria), ticketTypeRuleMatches(), ticketTypeUsable() (the types a company may give), ticketTypeRulesFor() (company's own, then shared), ticketTypeAutoPick(), ticketTypeAutoRecord() (the history line)
api/tickets/ticket_type_rules.php Settings API: GET, and POST save, delete, move, set_default, test. Needs Cap::TICKETS_TICKET_TYPES
assets/js/ticket-type-rules.js, assets/css/ticket-type-rules.css The section under Tickets → Settings → Ticket types and its editor
workflow/includes/engine.php The set_ticket_type action, through action_ticket_field()
tests/ticket-type-rules.php Criteria, order, default, a chosen type winning, the service, the Workflow action; two companies in a child process

Table ticket_type_rules (tenant_id NULL = shared, name, ticket_type_id, conditions_json, display_order, is_active, created_by); setting ticket_default_type_id through tenantSetting() - a company's row in tenant_settings, else system_settings. A company row of '0' means "none for this company"; no row means "follow the shared default".

2. 🔑 Why before the insert, and why not Workflow alone

The issue offered a default or a Workflow action. A Workflow action runs after the ticket exists, which is too late for three things:

  • The number. TicketNumbering::next($conn, $typeId, $tenantId) can put the type in the number ({TYPE}) or count per type. A type set afterwards leaves an incident number on a service request.
  • Channels that never announce. Only email, the portal and TicketsService::createTicket() fire ticket.created; web chat, messaging, the catalogue and a workflow's own "Create a ticket" do not. A rule there would silently never run.
  • Things that hang off the type - categories bound to a type, custom fields per type.

So the decision is a plain function every creation path calls before TicketNumbering::next(), and Workflow gets set_ticket_type for later changes.

3. 🚪 The paths, and what each passes

ticketTypeAutoPick($conn, $tenantId, $facts) is called only when no type was chosen. $facts: channel, mailbox_id, origin_id, department_id, subject, requester_email - whatever that path knows at that moment.

Path File channel Notes
New ticket form, REST API, alerts, 3CX, projects, tasks includes/services/tickets.php createTicket(..., ?string $channel) form from the UI, api otherwise, or what the caller passes: monitoring (Zabbix, Domains renewal), phone (3CX), internal (project RAID issue, task follow-up) A parameter, never a key in $in, so an API caller cannot claim to be email
Email api/tickets/check_mailbox_email.php email Mailbox, the mailbox's origin, subject, sender; generateTicketNumber() now takes the type
Portal api/self-service/create_ticket.php portal The chosen mailbox, subject, requester
Messaging includes/messaging/ingest.php messaging Origin and the built subject; messagingGenerateTicketNumber() now takes the type
Web chat includes/webchat/webchat.php webchat
Catalogue includes/catalogue_approvals.php catalogueCreateTicketFromSubmission() catalogue, or workflow when a workflow raises from a form (_channel in the overrides) Only when the form / rule configured no type
Workflow "Create a ticket" workflow/includes/engine.php workflow Only when the step names no type

Merge and split copy the type from the tickets they came from, so they do not ask.

TRAP: in createTicket() the pick comes AFTER the custom-field and category checks. Those were made against the type the caller sent (none) - which is what the form showed. Picking first would make a required-at-logging field of the rule's type required, and refuse an analyst for a field they were never shown.

TRAP: ticketTypeAutoPick() never throws. A missing table, a bad row, anything: it logs and returns null, and the ticket is raised as before. Ticket creation must never depend on this feature working.

4. 🧮 Matching

  • ticketTypeRulesFor($tenantId): the company's rules (tenant_id = ?) by display_order, id, then the shared ones. Single-company installs only have shared rules (the settings save with tenant_id NULL in the default context, as ticket types do).
  • A rule is passed over when its type is not in ticketTypeUsable($tenantId) - inactive, hidden by the company (tenant_config_hidden), or another company's - or when it has no criteria at all (the API refuses those; this is belt and braces).
  • ticketTypeRuleMatches(): an empty criterion is ignored; a criterion whose fact is unknown (department_id null) does not match; lists match any; subject lines match case-insensitively anywhere; a domain matches itself or *.domain.
  • A triage ticket (tenant_id null on a multi-company install) can only get a shared type.

5. 🧾 History

ticketTypeAutoRecord() writes a ticket_audit row, field_name Ticket Type (the same name a manual change uses), analyst_id NULL, new_value like Service Request (set by the rule "Requests mailbox"). Stored in English, as the rest of the trail is.

6. ⚙️ Settings API

Scope follows ticket types: single-company, or the default company on a multi-company install → shared rules and system_settings; a client company → its own rules and tenant_settings (set_default value inherit deletes its row, '' stores '0'). move renumbers the scope 10, 20, 30... and swaps, so equal orders can never make a move do nothing. test runs ticketTypeAutoPick() itself - the answer is the real one. The Company criterion is kept only on shared rules of a multi-company install.

7. 🔁 Workflow: set_ticket_type

action_ticket_field($args, $payload, 'ticket_type_id', 'ticket_type_id', 'type') - the same route as team, department and category: checked against ticketTypeUsable() for the ticket's company, then TicketsService::updateTicket(), which audits, fires events, and clears a category bound to another type. The number is not changed.

8. ✅ How it was checked

tests/ticket-type-rules.php (33 checks on the test install): criteria cleaning and matching; first match, an inactive rule and a rule naming a retired type passed over, the default; a ticket through the real createTicket() getting the rule's type with the history line, a chosen type winning with no line, an API ticket counted as REST API; set_ticket_type changing a type and refusing one the company cannot use; then, with a second company in a child process, a company's rules before the shared ones, a shared rule naming a type the company hid passed over, a company's own default, "none" and the shared one. The settings section was driven in a browser: refusing a rule with nothing filled in, saving, the summary, the default, reordering, and Try it naming the rule and falling to the default. workflow-company-scope (87), ticket-numbering, the portal, messaging, 3CX and catalogue tests were run after the change.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally