Skip to content

SLA Per Company Developer Guide

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

πŸ› οΈ Per-company SLAs - Developer Guide

Since 5.3.0 Β· #320 Β· PR #331 by nebev (review) Β· User guide: SLA Management β†’ Per-company priorities and calendars

Priorities and SLA business calendars can belong to one company, beside the shared ones - the same shared-plus-own model ticket_types already had, not a new SLA-policy table.

πŸ“ Files

File Role
ticket_priorities.tenant_id, sla_calendars.tenant_id NULL = shared, a value = that company's own. Unique keys are now (tenant_id, name); FKs to tenants ON DELETE CASCADE, as for ticket types
includes/ticket_priorities.php Every read and rule goes through here: ticketPrioritiesFor(), ticketPrioritiesShared(), ticketPrioritiesOwn(), ticketPriorityDefaultId(), ticketPriorityForNewTicket(), ticketPriorityFollowCompanyMove(), slaCalendarsFor(), slaCalendarsOwnedBy(), slaDefaultCalendarId(), tenantScopeRequireAccess(), and the readiness guard ticketPriorityTenancyReady()
includes/sla.php sla_get_state(): the priority's calendar, else slaDefaultCalendarId() for the ticket's company
includes/services/tickets.php resolvePriority() - an id or a name, among the ticket's company's priorities only; 422 Unknown priority otherwise
api/tickets/*priorit*, *sla_calendar*, get_sla_settings.php, set_ticket_priority_hidden.php Settings; each write checked against the row's stored tenant_id
Channels: api/self-service/create_ticket.php, includes/webchat/webchat.php, includes/messaging/ingest.php, api/tickets/check_mailbox_email.php, includes/catalogue_approvals.php, includes/integrations/zabbix.php, workflow/includes/engine.php New tickets start on ticketPriorityForNewTicket()
api/v1/resources/reference.php GET /priorities?company_id= - that company's view; 403 for a key that cannot reach it; without it, unchanged
api/system/db_verify.php The upgrade: columns, the unique-key swap name β†’ (tenant_id, name), FKs; existing rows become shared and keep their defaults
tests/ticket-priority-tenancy.php 74 checks in four parts - see Β§6

πŸ”‘ The rules

  • What a company sees: its own priorities plus the shared ones it has not hidden (tenant_config_hidden, entity_type = 'ticket_priority'). Calendars: its own plus every shared one - calendars are not hidden, because hiding the shared default would silently leave a company with no SLA.
  • Defaults are per scope: one shared default, at most one per company. A new ticket: the company's default, else the shared default, else its first priority.
  • A shared priority can only use shared calendars - otherwise one company's hours would run every company's tickets. A company's own priority can use the shared ones and its own.
  • The calendar that runs the clock: the priority's own; else the company's default calendar; else the shared default; else no SLA. Never "the first calendar".
  • Moving a ticket to a company that cannot use its priority resets it to that company's default, on the history, raising ticket.priority_changed - in ticketsFollowCompanyMove(), so every move path (Move to company, triage, the service) gets it.
  • Who may change a row: a shared priority or calendar needs access to every company (analystHasAllTenantAccess()); a company's own needs that company. Stricter than ticket types on purpose (Ed, 2026-11-14), like shared asset locations.
  • The Default company has no rows of its own - its list is the shared list, as for ticket types.

⚠️ TRAP: before Database Verification

Until Database Verification adds the two tenant_id columns, an install running the new code would have selected a column that does not exist - new tickets with no priority, every priority refused, SLA state failing for every ticket. ticketPriorityTenancyReady() (cached, INFORMATION_SCHEMA) gates every read: until it is true, every row counts as shared, nothing is hidden, and the settings screens behave as single-company. Writes only set tenant_id when the column exists. Test part 4 (--unverified) drops the columns, proves this, and restores them exactly.

βš™οΈ Edge cases settled on merge

  • Resending the current priority (a REST PATCH carrying the unchanged priority_id, perhaps one the company has since hidden) is treated as no change when the same request does not move the ticket - not refused.
  • Showing a hidden shared priority is refused while the company has its own with the same name, so it never sees two "Urgent"s.
  • Install-wide integration settings (Zabbix default severity map, Teams minimum priority) offer shared priorities only. The tracker priority mapping lists every priority, labelled "P1 (Acme)" on a multi-company install, because each ticket's own priority needs a row there.

πŸ§ͺ Verifying

FREEITSM_TEST_DATABASE=freeitsm_prtest FREEITSM_BASE_URL=http://localhost/freeitsm-prtest php tests/ticket-priority-tenancy.php

Parts 1-2 run in a rolled-back transaction (visibility, defaults, resolvePriority by id and name, moves, the calendar chain, channels); part 3 (workflows) commits its ZZ320 WF rows and deletes them by id; part 4 is the pre-verification state. Not covered by it: every HTTP endpoint end to end and the REST create/update path - the service-level checks stand in for them.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Responses to feedback

Blue sky thinking

Bugs resolved

Pull requests

Links

Clone this wiki locally