Repository navigation
Ticket Types For New Tickets 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.
| 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".
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()fireticket.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.
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 |
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.
-
ticketTypeRulesFor($tenantId): the company's rules (tenant_id = ?) bydisplay_order, id, then the shared ones. Single-company installs only have shared rules (the settings save withtenant_idNULL 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_idnull) does not match; lists match any; subject lines match case-insensitively anywhere; a domain matches itself or*.domain. - A triage ticket (
tenant_idnull on a multi-company install) can only get a shared type.
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.
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.
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.
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 — an open-source IT Service Management platform · github.com/edmozley/freeitsm · MIT licence
- Installation
- ⏰ Scheduled tasks (cron jobs)
-
Architecture
- ↳ 🧹 Root folder tidy (URL routing)
- ↳ 🔗 Database Integrity (FKs & constraints)
- ↳ 🛠️ Database Verification — Developer Guide
- ↳ 🧩 Service Layer (UI + API share code)
- ↳ 🪤 Code traps (the TRAP: comments)
- ↳ 📝 Commenting for the Engineering wiki
- ↳ 🛠️ Engineering wiki scanner - Developer Guide
- ↳ 📊 Service-Layer progress
- 🤝 Contributing (read before a pull request)
- 🧪 Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- 📅 Date & Time Formats
- 💱 Currencies
- 🧭 The sidebar (optional module rail)
- 🧩 Preference templates
- 🔀 Display order (modules, Watchtower cards) — Developer Guide
- Theming & Dark Mode
- 🗂️ Recent — getting back to what you were doing
- ⌨️ Command palette (⌘K)
- 🔍 Searching inside tickets
- 📄 Attached documents
-
Mobile‑Friendly
- ↳ 🎫 Mobile: Tickets
- ↳ 💻 Mobile: Assets
- ↳ 📅 Mobile: Calendar
- ↳ 📚 Mobile: Knowledge
- ↳ 🚦 Mobile: Service Status
- ↳ 🗼 Mobile: Watchtower
- ↳ 🧩 Mobile: Problem Management
- ↳ 🔁 Mobile: Change Management
- ↳ 💿 Mobile: Software
- ↳ ✅ Mobile: Tasks
- ↳ 📝 Mobile: Forms
- ↳ 📄 Mobile: Contracts
- ↳ 📄 Mobile: Domains
- ↳ 📄 Mobile: People
- ↳ 🚀 Mobile: Projects
- ↳ 🗂️ Mobile: Files
- ↳ 🎓 Mobile: LMS
- ↳ 🗺️ Mobile: CMDB
- ↳ 🗺️ Mobile: Network Mapper
- ↳ 🧭 Mobile: Process Mapper
- ↳ ⚙️ Mobile: Workflow
- ↳ 🖥️ Mobile: System
- ↳ 📊 Mobile: Reporting
- ↳ 📖 Mobile: System Wiki
- ↳ 🙋 Mobile: Self-Service Portal
- ↳ 🧰 Mobile: Techniques & Tricks
-
Security
- Layer 1 — which modules you can enter
- ↳ 🧩 Module Access Control
- ↳ 🛠️ Module Access — Developer Guide
- ↳ 🔌 Switching modules off
- ↳ 🛠️ Module availability — Developer Guide
- ↳ 📐 Module availability — design (#217)
- Layer 2 — what you can administer
- ↳ 🎭 Roles & Permissions
- ↳ 🛠️ Roles — Developer Guide
- ↳ 🔤 Why capabilities are constants
- Layer 3 — the System module
- ↳ 🔑 Admin Access Control
- Hardening
- ↳ 📄 Security review response 2026-08
- ↳ 🛡️ Security hardening 2026-08
- ↳ 🛠️ Security hardening 2026-08 — Developer Guide
- ↳ 🛡️ Round three — plain English
- ↳ 🛠️ Round three — Developer Guide
- ↳ 🛡️ CSRF protection (S4) — Developer Guide
- Single Sign-On (SSO)
- 🗂️ LDAP & Active Directory
- 📇 CardDAV contact sync
- Browser Extension
- API Reference
-
🔌 REST API — how it works
- ↳ 📋 Proposal: custom fields, inventory import and change evidence (#194)
- ↳ 🛠️ Custom fields, evidence + inventory - review (#195-#197)
- ↳ 🛠️ Long forms in a task's Work view - review (#198)
- ↳ 🛠️ Microsoft Entra ID import - review (#235)
- ↳ 🛠️ Assets from their Intune primary user - review (#236)
- ↳ 🛠️ Organisation chart - review (#242)
- ↳ 🎫 REST API: Tickets
- ↳ 💻 REST API: Assets
- ↳ 🔴 REST API: Problems
- ↳ 🟠 REST API: Changes
- ↳ 📚 REST API: Knowledge
- ↳ ✅ REST API: Tasks
- ↳ 🗄️ REST API: CMDB
- ↳ 📜 REST API: Contracts
- ↳ 🗓️ REST API: Calendar
- ↳ 💿 REST API: Software
- ↳ 🌐 REST API: Domains
- ↳ 🚦 REST API: Service Status
- ↳ ☀️ REST API: Morning Checks
- ↳ 📝 REST API: Forms
- ↳ ⚙️ REST API: Workflow
- ↳ 🏷️ REST API: Cost centres
- ↳ 🧑💼 REST API: Analysts
- ↳ 🗺️ REST API: Network Mapper
- ↳ 🧭 Using the API docs page
- ↳ 📐 OpenAPI specification
- ↳ ✅ OpenAPI: kept correct
- ↳ 🛠️ Maintaining the catalogue
- Watchtower
-
Tickets
- ↳ 🛠️ Inbox.js map — Developer Guide
- ↳ 🛠️ Floating windows & the file viewer — Developer Guide
- ↳ 🛠️ Tickets settings — Developer Guide
- ↳ 📐 Simpler ticket and task forms - design (#218)
- ↳ 🗂️ The ticket workspace
- ↳ 📊 The ticket table
- ↳ 📋 Rota copy and paste — Developer Deep Dive
- ↳ ✅ Checklists & SOPs
- ↳ ☑️ Mandatory fields
- ↳ 🧾 Custom ticket fields
- ↳ 🏷️ Ticket categories
- ↳ 🧾 A type for every new ticket
- ↳ 👥 Assigning tickets to a team, and escalation
- ↳ 🏢 One board across every company
- ↳ Mailbox Authentication
- ↳ 📤 Email send log
- ↳ 📎 Attachment size limits and large emails
- ↳ Basic IMAP mailboxes
- ↳ Email rendering & images
- ↳ SLA Management
- ↳ WhatsApp channel
-
↳
✈️ Telegram channel - ↳ ⭐ CSAT company scope and filters — Developer Guide
- ↳ 👥 Microsoft Teams channel
- ↳ 🗨️ Mattermost channel
- ↳ 💬 Web chat channel
- ↳ 🟣 Slack channel
- ↳ 🔗 Linking tickets
- ↳ ⓘ Record previews
- ↳ 📝 Ticket notes: internal or shared
- ↳ 🗒️ Canned responses
- ↳ ✉️ Limiting replies to particular senders
- ↳ 📨 Telling the analyst a ticket is theirs
- ↳ ✍️ Email signatures
- ↳ 🌐 The public web address
- ↳ 🔢 Ticket numbering
- ↳ 🙋 Raising a ticket for someone else
- ↳ 🔀 Merging tickets
- ↳ 🔒 Confidential tickets
- ↳ 👥 Portal managers
- ↳ 👁 Who has seen a ticket
- ↳ 📜 Reading long tickets
- ↳ ⑂ Splitting tickets
- ↳ ✅ Selecting several tickets
- ↳ 🗂️ The folder pane
- ↳ 🔽 Just my tickets, or no closed ones
- ↳ 🛠️ Snoozing tickets — Developer Guide
- ↳ 👥 Collision detection
- ↳ ⏱️ Time tracking
- ↳ 📄 Case reports - a ticket and its work as one PDF
- ↳ 📅 Scheduled work in your own calendar
- Problem Management
- Tasks
- 🚀 Projects
-
Assets
- ↳ 🏢 Moving an asset between companies
- ↳ 📍 Shared asset locations
- ↳ 🧑💼 Assigning assets to analysts
- ↳ 📆 Warranty and lease alerts
- ↳ 📄 Leases
- ↳ 🔭 Saved table views
- ↳ 🖨️ Recording anything, and importing it
- ↳ 🧩 Asset type layouts
- ↳ 🕒 Inventory freshness
- ↳ 🔗 Linked configuration item
- ↳ 🔌 Connected assets
- ↳ 🔍 Asset review
- ↳ 🏷️ QR asset labels
- ↳ 📋 Who holds what, and handover documents
- ↳ 🖥️ The inventory agent (PowerShell)
- ↳ 🗄️ Proxmox VE servers
- ↳ ☁️ VMware Cloud Director servers
- ↳ 🪟 Hyper-V and Failover Clusters
- ↳ 🔗 Linking equipment to tickets
- ↳ ☑️ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- ↳ 🎨 The form designer — Developer Guide
- ↳ 📐 Layout & the grid — Developer Guide
- ↳ 🗂️ Collections — grouping submissions
- ↳ 📄 Submissions as PDFs
- ↳ ⚡ What happens next — a form's own actions
- ↳ 🛠️ Sections & conditional logic — Developer Guide
- ↳ 🛠️ Lookup fields — Developer Guide
- ↳ 🛡️ Catalogue request approvals
- People
- Starters & leavers
- Domains
- 🗂️ Files
- Contracts
- Service Status
- 🔔 Notifications
- 🚨 War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- 📡 Zabbix (monitoring)
- 💬 Microsoft Teams alerts
- 📞 3CX (phone system)
- System
-
Overview
- ↳ 📊 Progress tracker
- ↳ Concepts & vocabulary
- ↳ Email routing & mailboxes
- ↳ Settings: global vs per-company
- ↳ Users & self-service
- ↳ Staff cross-company access
- ↳ 🏢 One board across every company
- ↳ Worked examples
- ↳ Pitfalls & gotchas
- ↳ Scope: what it's for
- ↳ 🛠️ Developer Guide (make a module multi-company)
- ↳ 🗄️ Case study: CMDB (a linked graph)
- ↳ 🧪 Test harness (prove it's isolated)
- What this is
-
🐞 Bugs resolved
- ↳ 🔒 A lookup answer could name any record
- ↳ 🎫 #227 A mailbox could not be deleted
- ↳ 🎫 #226 A picture on a note could not be closed
- ↳ 💻 #222 The Assets page never loaded in 4.3.0
- ↳ ⚙️ #214 A "contains" condition did not match capitals
- ↳ ⚙️ #216 A workflow email from a chosen mailbox always failed
- ↳ 🐳 The Docker image was missing PHP extensions
- ↳ 🌐 A failed DNS lookup read as a missing record
- ↳ 🔒 A link could change data
- ↳ 🔒 Access outlived its removal
- ↳ 🔒 Knowledge articles could run scripts
- ↳ 🔒 Knowledge writes ignored permissions
- ↳ 🔒 Company boundaries (Oct 2026)
- ↳ 🔒 Smaller gaps (Oct 2026)
- ↳ 🔒 Escaped text could leave its attribute
- ↳ 🔒 Task descriptions were not cleaned
- ↳ 🔒 Task documents ignored company scope
- ↳ 🔒 Repeating tasks ignored company scope
- ↳ 📬 #183 Mailbox checks overlapped
- ↳ 🌐 #188 Transfer lock shown off when unknown
- ↳ ✅ #184 Subtask names and Overdue
- ↳ 🧩 #186 Teams: the buttons did nothing
- ↳ 🔒 Text in a change could run script
- ↳ 🔒 AI settings changed without the AI permission
- ↳ 🚪 #178 Deleting an analyst failed
- ↳ 🎫 A team with no departments showed 0 tickets
- ↳ 🚀 Projects: adding a phase did nothing on Save
- ↳ 🔒 Leavers could still sign in
- ↳ 🖼️ Logo and courses broke on Apache with PHP-FPM
- ↳ 🔢 Chat tickets ignored your ticket numbering
- ↳ 📅 Dates shown as a dash, or in server time
- ↳ 🔒 Assets → Users showed people from other companies
- ↳ 🔒 Restricted analysts could read other modules' data
- ↳ 🖼️ Replies with a picture in the thread failed to send
- ↳ 📎 Reply attachments never reached the customer
- ↳ 🛠️ Outbound email attachments — Developer Guide
- ↳ 🔑 A global SSO provider was missing from the portal
- ↳ 🔀 Behind a proxy, the SSO redirect said http
- ↳ ✏️ The portal tagline moved when you saved it
- ↳ 🎨 The portal settings screen forgot what you saved
- ↳ 🛡️ The approvals inbox said "Error" and nothing else
- ↳ 📄 A table's answers were missing from the PDF
- ↳ ◉ A single-select column let you tick every option
- ↳ 📐 The portal ignored a form's field widths
- ↳ 📋 The tasks board stopped taking clicks
- ↳ 🗂️ #121 The index list is out of date after upgrading
- ↳ 📅 #133 The calendar subscription was empty
- ↳ 📋 #131 Tasks always reopened on the board
- ↳ 💥 #129 Every page returned HTTP 500 after upgrading
- ↳ 🐳 #127 A PHP warning above the System page
- ↳ 🕐 #126 Notes stamped with the server's clock
- ↳ 🌍 Storing every date in UTC
- ↳ 🚪 The portal was down for everyone signed in
- ↳ ⚙️ #120 Workflow notes could never be written
- ↳ ⚙️ #123 Three errors when running Database Verification
- ↳ 📝 #122 The description box was a stub in the corner
- ↳ 💣 Demo data deleted real accounts
- ↳ 🔐 #117 Sign-in redirected to the wrong address
- ↳ 🎨 #108 The priority dot was invisible
- ↳ ⏱️ #116 Time logged from the right-click menu
- ↳ 🔑 #114 API keys refused by our own guard
- ↳ 🗂️ #110 Assigning a task told nobody
- ↳ 🚪 #107 Signed out while still working
- ↳ 📎 #103 "Share with Requester" reached nobody
- ↳ 🔍 #102 Search found nothing for hyphens
- ↳ 🪟 #101 Source code editor opened behind
- ↳ ☑️ #88 Subtasks could not be ticked off
- ↳ 💻 #84 Asset deep link selected nothing
- ↳ 🎫 #79 A new ticket arrived with no status
- ↳ 📧 #79 A ticket from email did not say so
- ↳ 🔔 #78 Bell opened to nothing
- ↳ 📬 #77 Mail only collected from Inbox
- ↳ 🔐 #74 The default password could not be changed
- ↳ 🚦 #70 Renaming an impact level
- ↳ 📤 #67 App-only mailboxes could not send
- ↳ 📭 #45 Verify only ever worked for Microsoft
- ↳ 📭 #45 IMAP reported as not authenticated
- ↳ ✉️ An email template stopped escaping itself
- ↳ 🕐 The portal dashboard showed the wrong time
- ↳ 🔢 The folder said 99 and the list showed 96