Issue 248 Department Rules Never Matched On Four Routes - edmozley/freeitsm GitHub Wiki
A department rule never matched on portal, email, messaging or web chat tickets
Reported: #248, found while writing up #218, November 2026 ยท Module: Tickets ยท Fixed in: the release after 4.10.0 (changelog #2739)
What you saw
A ticket type rule (Tickets โ Settings โ Ticket types โ rules) can say "a new ticket in the IT department becomes an Incident". On a ticket raised in Tickets, that worked. On a ticket raised from the self-service portal, by email, from messaging (WhatsApp, Teams, Slack) or from web chat, the rule was skipped without any message, and the ticket got the next rule's type or the default.
Try it, at the bottom of the rules section, said the rule would match - because there you type the department in yourself. So the screen that is meant to prove a rule disagreed with what real tickets got.
What was actually wrong
Two things that each worked, in the wrong order
Since 4.9.0 (#218), a company can have a default department: a ticket that arrives without one starts there. The portal, email and chat never choose a department themselves, so on those routes the default is the only department a new ticket can have.
Each route makes a new ticket in roughly this order:
- pick the type - the type rules run here, given the facts the route knows;
- make the ticket number, which can carry the type;
- work out the department - the company's default;
- insert the ticket.
The type rules were asked at step 1 about a department that was only worked out at step 3. The routes did not pass a department at all, and a rule condition about a fact that is unknown does not match. That last part is deliberate: an unknown department must not count as "any department". So every department condition failed on these routes, default or not.
Why it was easy to miss
The default department and the type rules were built separately (#218 and #230), and each one was right on its own. The routes that create tickets from inside FreeITSM - New ticket in Tickets and the REST API, catalogue approvals and the workflow Create a ticket action - already worked out the department first, because a caller can name one there. Only the four routes that take tickets from outside had the old order.
It was four places, not one
A search for every caller of ticketTypeAutoPick() found eight. Four were broken and four were fine:
| Route | File | Before |
|---|---|---|
| Portal | api/self-service/create_ticket.php |
โ type first, no department passed |
api/tickets/check_mailbox_email.php |
โ type first, no department passed | |
| Messaging | includes/messaging/ingest.php |
โ type first, no department passed |
| Web chat | includes/webchat/webchat.php |
โ type first, no department passed |
| New ticket, REST API, alerts, 3CX | includes/services/tickets.php |
โ |
| Catalogue | includes/catalogue_approvals.php |
โ |
| Workflow "Create a ticket" | workflow/includes/engine.php |
โ |
| The Try it button | api/tickets/ticket_type_rules.php |
โ (you type the department) |
The fix
On each of the four routes, the department is now worked out before the type is picked, and passed to the rules:
// includes/webchat/webchat.php
[$departmentId, $departmentNote] = ticketDepartmentForNewTicket($conn, $tenantId, null);
require_once __DIR__ . '/../ticket_type_rules.php';
$typePick = ticketTypeAutoPick($conn, $tenantId, [
'channel' => 'webchat', 'origin_id' => $originId, 'subject' => $subject,
'requester_email' => $email, 'department_id' => $departmentId,
]);
Moving it earlier changes nothing else. ticketDepartmentForNewTicket() only reads - the company's default and whether that department is still active - and writes nothing. The history note it may produce ("the default department is no longer active") is still written after the insert, once there is a ticket to write it on.
No database change, nothing to run after upgrading.
Files changed
| File | Change |
|---|---|
๐ต api/self-service/create_ticket.php |
department before the type; passed to the rules |
๐ต api/tickets/check_mailbox_email.php |
the same |
๐ต includes/messaging/ingest.php |
the same |
๐ต includes/webchat/webchat.php |
the same |
๐งช tests/ticket-type-rules.php |
part 1b: every route works out the department before the type and passes it; a sweep fails if a new route calls ticketTypeAutoPick() without being checked |
Each moved block carries a TRAP: comment naming #248, so the order is not "tidied" back.
How it was proved
php tests/ticket-type-rules.php- 27 passed.- Control: the same test against the old code - all 8 checks on the four broken routes fail, and the three routes that were already right pass. So the test can tell the two apart; it is not passing because it checks nothing.
- The sweep found its own blind spot on the first run: it flagged
includes/ticket_type_rules.php, where the function is defined. That file is now listed as known, and any new caller still fails the test until somebody checks it.
Not proven: the test reads the source, it does not raise a real portal, email or chat ticket. The matching itself - a department condition against a department - was already tested in part 1. What changed is only whether the department reaches it, which is what the source check covers. A ticket raised end to end on a test install would be the stronger proof.
What this means for you
- If you have a type rule with a Department condition and a default department set, portal, email, messaging and web chat tickets now get that rule's type.
- On those routes the default is the only department a new ticket has, so a Department condition there means "the company's default department". Tickets created before the upgrade keep the type they were given.
- No rule needs changing.