diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b1293c..4c4a299a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 898271ef..fd57d4de 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -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' @@ -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); } diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index bd284550..d3a305a6 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -30,6 +30,8 @@ namespace GlpiPlugin\Escalade\Tests\Units; +use Calendar; +use CalendarSegment; use CommonITILActor; use CommonITILObject; use Entity; @@ -41,6 +43,8 @@ use PluginEscaladeTicket; use Rule; use RuleCommonITILObject; +use SLA; +use SLM; use Ticket; use Ticket_User; use User; @@ -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'], + ), + ); + } + } }