Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Prevent business rules from triggering twice on tickets
- Fixed routing issue caused by incorrect trailing slash
- Fix to rule execution based on group deletion

## [2.9.19] - 2026-27-01

Expand Down
41 changes: 40 additions & 1 deletion inc/ticket.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public static function pre_item_update(CommonDBTM $item)
return $item;
}

// Rules-only pass triggered by processAfterAddGroup: skip escalade logic entirely.
if (isset($item->input['_plugin_escalade_rules_only'])) {
return $item;
}

// Special case: If we have _itil_assign without actortype, we need to distinguish between:
// 1. "Associate myself" button - many fields merged from ticket form
// 2. History button escalation (climb_group) - specific structure with groups_id and _type = 'group'
Expand Down Expand Up @@ -492,8 +497,42 @@ public static function processAfterAddGroup(Group_Ticket $item)
$tickets_id = $item->fields['tickets_id'];
$groups_id = $item->fields['groups_id'];

//remove old groups (keep last assigned)
// Fire business rules before removing old groups: pass _actors with only the new
// group so GLPI detects old groups as deleted and rules see the final state.
// getFromDB() is required first so isNewItem() returns false and deleted-actor
// detection runs. _plugin_escalade_rules_only skips escalade logic in pre_item_update.
// Safety net in case updateActors() above did not already remove old groups.
if ($_SESSION['glpi_plugins']['escalade']['config']['remove_group'] == true) {
$all_actors = self::getTicketFieldsWithActors($tickets_id, $groups_id);

// Keep only the new group in the assign list (drop old ones).
$seen_new_group = false;
$all_actors['assign'] = array_values(array_filter(
$all_actors['assign'],
function (array $actor) use ($groups_id, &$seen_new_group): bool {
if ($actor['itemtype'] !== 'Group') {
return true;
}

if ($actor['items_id'] == $groups_id && !$seen_new_group) {
$seen_new_group = true;
return true;
}

return false;
},
));

$ticket_for_rules = new Ticket();
$ticket_for_rules->getFromDB($tickets_id);
$ticket_for_rules->update([
'id' => $tickets_id,
'_actors' => $all_actors,
'_plugin_escalade_rules_only' => true,
'_disablenotif' => true,
'actortype' => CommonITILActor::ASSIGN,
'groups_id' => $groups_id,
]);
self::removeAssignGroups($tickets_id, $groups_id);
}

Expand Down
120 changes: 120 additions & 0 deletions tests/Units/TicketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

namespace GlpiPlugin\Escalade\Tests\Units;

use Calendar;
use CalendarSegment;
use CommonITILActor;
use CommonITILObject;
use Entity;
Expand All @@ -41,6 +43,8 @@
use PluginEscaladeTicket;
use Rule;
use RuleCommonITILObject;
use SLA;
use SLM;
use Ticket;
use Ticket_User;
use User;
Expand Down Expand Up @@ -1339,4 +1343,120 @@ public function testRuleCreatesTaskWhenCategoryAssigned()
$category->delete(['id' => $category_id], true);
$rule->delete(['id' => $rule_id], true);
}

public function testSlaRuleDuringEscalation(): void
{
$this->initConfig([
'remove_group' => 1,
'show_history' => 1,
]);

$entity_id = getItemByTypeName('Entity', '_test_root_entity', true);

$calendar = $this->createItem(Calendar::class, ['name' => __FUNCTION__]);
$this->createItems(CalendarSegment::class, [
['calendars_id' => $calendar->getID(), 'day' => 1, 'begin' => '08:00:00', 'end' => '18:00:00'],
['calendars_id' => $calendar->getID(), 'day' => 2, 'begin' => '08:00:00', 'end' => '18:00:00'],
['calendars_id' => $calendar->getID(), 'day' => 3, 'begin' => '08:00:00', 'end' => '18:00:00'],
['calendars_id' => $calendar->getID(), 'day' => 4, 'begin' => '08:00:00', 'end' => '18:00:00'],
['calendars_id' => $calendar->getID(), 'day' => 5, 'begin' => '08:00:00', 'end' => '18:00:00'],
]);

$slm = $this->createItem(SLM::class, [
'name' => __FUNCTION__,
'entities_id' => $entity_id,
'is_recursive' => true,
'calendars_id' => $calendar->getID(),
]);
$sla_tto = $this->createItem(SLA::class, [
'name' => __FUNCTION__ . ' TTO',
'entities_id' => $entity_id,
'is_recursive' => true,
'type' => SLM::TTO,
'number_time' => 4,
'definition_time' => 'hour',
'slms_id' => $slm->getID(),
]);

$group1 = $this->createGroup('Groupe 1');
$group2 = $this->createGroup('Groupe 2');

// Rule 1 (ONADD): if assigned group IS "Groupe 1" → assign SLA TTO
$rule_add = $this->createItem('Rule', [
'name' => __FUNCTION__ . ' assign SLA on add',
'sub_type' => 'RuleTicket',
'match' => 'AND',
'is_active' => 1,
'condition' => RuleCommonITILObject::ONADD,
'entities_id' => $entity_id,
]);
$this->createItem('RuleCriteria', [
'rules_id' => $rule_add->getID(),
'criteria' => '_groups_id_assign',
'condition' => Rule::PATTERN_IS,
'pattern' => $group1->getID(),
]);
$this->createItem('RuleAction', [
'rules_id' => $rule_add->getID(),
'action_type' => 'assign',
'field' => 'slas_id_tto',
'value' => $sla_tto->getID(),
]);

// Rule 2 (ONUPDATE): if assigned group IS NOT "Groupe 1" → set SLA TTO to 0
$rule_update = $this->createItem('Rule', [
'name' => __FUNCTION__ . ' remove SLA on update',
'sub_type' => 'RuleTicket',
'match' => 'AND',
'is_active' => 1,
'condition' => RuleCommonITILObject::ONUPDATE,
'entities_id' => $entity_id,
]);
$this->createItem('RuleCriteria', [
'rules_id' => $rule_update->getID(),
'criteria' => '_groups_id_assign',
'condition' => Rule::PATTERN_IS_NOT,
'pattern' => $group1->getID(),
]);
$this->createItem('RuleAction', [
'rules_id' => $rule_update->getID(),
'action_type' => 'assign',
'field' => 'slas_id_tto',
'value' => 0,
]);

foreach ($this->escalateTicketMethods(['escalateWithTimelineButton', 'escalateWithHistoryButton']) as $data) {
// Create ticket with Groupe 1 → SLA TTO should be set by Rule 1
$ticket = $this->createItem('Ticket', [
'name' => __FUNCTION__,
'content' => __FUNCTION__,
'entities_id' => $entity_id,
'_actors' => [
'assign' => [
['itemtype' => 'Group', 'items_id' => $group1->getID()],
],
],
]);

$ticket->getFromDB($ticket->getID());
$this->assertEquals(
$sla_tto->getID(),
$ticket->fields['slas_id_tto'],
'SLA TTO should be assigned on ticket creation with Groupe 1',
);

// Escalate to Groupe 2 → Rule 2 should remove SLA TTO
$this->{$data['method']}($ticket, $group2);

$ticket->getFromDB($ticket->getID());
$this->assertEquals(
0,
$ticket->fields['slas_id_tto'],
sprintf(
'SLA TTO should be removed after escalation to Groupe 2 via %s',
$data['method'],
),
);
}
}
}