From 4513f8170b6475824401bec033a8d5ac1a723fea Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Fri, 24 Apr 2026 11:47:10 -0500 Subject: [PATCH 1/9] fix credit consumption validation --- front/ticket.form.php | 49 +++---- inc/entity.class.php | 37 +++-- inc/ticket.class.php | 244 +++++++++++++++++++++++++------ templates/tickets/form.html.twig | 4 + 4 files changed, 254 insertions(+), 80 deletions(-) diff --git a/front/ticket.form.php b/front/ticket.form.php index c8a2dc1..f72cf86 100644 --- a/front/ticket.form.php +++ b/front/ticket.form.php @@ -31,36 +31,29 @@ use Glpi\Exception\Http\BadRequestHttpException; -Session::haveRight("ticket", UPDATE); +Session::checkLoginUser(); -$PluginCreditTicket = new PluginCreditTicket(); -if ($_REQUEST['plugin_credit_entities_id'] == 0) { - Session::addMessageAfterRedirect( - __s('Credit voucher entity must be selected.', 'credit'), - true, - ERROR, - ); - Html::back(); -} elseif ($_REQUEST['plugin_credit_quantity'] == 0) { - Session::addMessageAfterRedirect( - __s('Credit voucher quantity must be greater than 0.', 'credit'), - true, - ERROR, - ); - Html::back(); +if (!Session::haveRight('ticket', UPDATE) && !Session::haveRight(Entity::$rightname, UPDATE)) { + throw new BadRequestHttpException(); } -$input = [ - 'tickets_id' => $_REQUEST['tickets_id'], - 'plugin_credit_entities_id' => $_REQUEST['plugin_credit_entities_id'], - 'consumed' => $_REQUEST['plugin_credit_quantity'], - 'users_id' => Session::getLoginUserID(), -]; -if ($PluginCreditTicket->add($input)) { - Session::addMessageAfterRedirect( - __s('Credit voucher successfully added.', 'credit'), - true, - INFO, - ); + +$PluginCreditTicket = new PluginCreditTicket(); +if (isset($_POST["add"])) { + $input = [ + 'tickets_id' => $_POST['tickets_id'] ?? null, + 'plugin_credit_entities_id' => $_POST['plugin_credit_entities_id'] ?? null, + 'consumed' => $_POST['plugin_credit_quantity'] ?? null, + 'users_id' => Session::getLoginUserID(), + ]; + + if ($PluginCreditTicket->add($input)) { + Session::addMessageAfterRedirect( + __s('Credit voucher successfully added.', 'credit'), + true, + INFO, + ); + } + Html::back(); } diff --git a/inc/entity.class.php b/inc/entity.class.php index 5c13857..7cbabf7 100644 --- a/inc/entity.class.php +++ b/inc/entity.class.php @@ -569,14 +569,29 @@ public static function getActiveFilter() global $DB; return [ 'glpi_plugin_credit_entities.is_active' => 1, - 'OR' => [ - 'glpi_plugin_credit_entities.end_date' => null, - new QueryExpression( - sprintf( - 'NOW() < %s', - $DB->quoteName('glpi_plugin_credit_entities.end_date'), - ), - ), + 'AND' => [ + [ + 'OR' => [ + 'glpi_plugin_credit_entities.begin_date' => null, + new QueryExpression( + sprintf( + '%s <= NOW()', + $DB->quoteName('glpi_plugin_credit_entities.begin_date'), + ), + ), + ], + ], + [ + 'OR' => [ + 'glpi_plugin_credit_entities.end_date' => null, + new QueryExpression( + sprintf( + 'NOW() <= %s', + $DB->quoteName('glpi_plugin_credit_entities.end_date'), + ), + ), + ], + ], ], ]; } @@ -591,9 +606,13 @@ public static function getMaximumConsumptionForCredit(int $credit_id) 'FROM' => 'glpi_plugin_credit_entities', 'WHERE' => [ 'id' => $credit_id, - ], + ] + self::getActiveFilter(), ]; $entity_result = $DB->request($entity_query)->current(); + if ($entity_result === false) { + return 0; + } + $overconsumption_allowed = $entity_result['overconsumption_allowed']; $quantity_sold = (int) $entity_result['quantity']; diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 76370b7..165d6dd 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -190,6 +190,203 @@ public static function getConsumedForCreditEntity($ID) return $tot; } + private static function canUpdateCreditsForTicket(Ticket $ticket): bool + { + if (Session::haveRight(Entity::$rightname, UPDATE)) { + return true; + } + + return $ticket->canEdit($ticket->getID()) + && !in_array($ticket->fields['status'], array_merge(Ticket::getSolvedStatusArray(), Ticket::getClosedStatusArray())); + } + + private static function getValidatedConsumptionInput(int $ticket_id, int $credit_id, int $consumed, ?self $current_credit = null): array|false + { + $ticket = new Ticket(); + if ( + !$ticket->getFromDB($ticket_id) + || !$ticket->can($ticket_id, READ) + || !self::canUpdateCreditsForTicket($ticket) + ) { + Session::addMessageAfterRedirect( + __s('You do not have rights to update credit vouchers for this ticket.', 'credit'), + true, + ERROR, + ); + return false; + } + + $credit_entity = new PluginCreditEntity(); + $credit_criteria = getEntitiesRestrictCriteria('', '', $ticket->getEntityID(), true); + $credit_criteria['id'] = $credit_id; + $credit_criteria += PluginCreditEntity::getActiveFilter(); + + if (!$credit_entity->getFromDBByCrit($credit_criteria)) { + Session::addMessageAfterRedirect( + __s('Selected credit voucher is not available for this ticket.', 'credit'), + true, + ERROR, + ); + return false; + } + + $quantity_sold = (int) $credit_entity->fields['quantity']; + $quantity_consumed = self::getConsumedForCreditEntity($credit_entity->getID()); + + if ( + $current_credit instanceof self + && (int) $current_credit->fields['plugin_credit_entities_id'] === $credit_entity->getID() + ) { + $quantity_consumed = max(0, $quantity_consumed - (int) $current_credit->fields['consumed']); + } + + $quantity_remaining = max(0, $quantity_sold - $quantity_consumed); + + if (0 !== $quantity_sold && $quantity_remaining < $consumed) { + $message = sprintf( + __s('Quantity consumed exceeds remaining credits: %d', 'credit'), + $quantity_remaining, + ); + + if ($credit_entity->getField('overconsumption_allowed')) { + Session::addMessageAfterRedirect($message, true, WARNING); + } else { + Session::addMessageAfterRedirect($message, true, ERROR); + return false; + } + } + + return [ + 'tickets_id' => $ticket->getID(), + 'plugin_credit_entities_id' => $credit_entity->getID(), + 'consumed' => $consumed, + ]; + } + + public function prepareInputForAdd($input) + { + $ticket_id = filter_var( + $input['tickets_id'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($ticket_id === false) { + Session::addMessageAfterRedirect( + __s('Ticket is mandatory.', 'credit'), + true, + ERROR, + ); + return false; + } + + $credit_id = filter_var( + $input['plugin_credit_entities_id'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($credit_id === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher entity must be selected.', 'credit'), + true, + ERROR, + ); + return false; + } + + $consumed = filter_var( + $input['consumed'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($consumed === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher quantity must be greater than 0.', 'credit'), + true, + ERROR, + ); + return false; + } + + $validated_input = self::getValidatedConsumptionInput($ticket_id, $credit_id, $consumed); + if ($validated_input === false) { + return false; + } + + $input = $validated_input + $input; + $input['users_id'] = (int) Session::getLoginUserID(); + + return $input; + } + + public function prepareInputForUpdate($input) + { + $credit = new self(); + if ( + !isset($input['id']) + || filter_var($input['id'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) === false + || !$credit->getFromDB((int) $input['id']) + ) { + Session::addMessageAfterRedirect( + __s('Unable to find the requested credit consumption.', 'credit'), + true, + ERROR, + ); + return false; + } + + $ticket_id = filter_var( + $input['tickets_id'] ?? $credit->fields['tickets_id'], + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($ticket_id === false) { + Session::addMessageAfterRedirect( + __s('Ticket is mandatory.', 'credit'), + true, + ERROR, + ); + return false; + } + + $credit_id = filter_var( + $input['plugin_credit_entities_id'] ?? $credit->fields['plugin_credit_entities_id'], + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($credit_id === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher entity must be selected.', 'credit'), + true, + ERROR, + ); + return false; + } + + $consumed = filter_var( + $input['consumed'] ?? $credit->fields['consumed'], + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($consumed === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher quantity must be greater than 0.', 'credit'), + true, + ERROR, + ); + return false; + } + + $validated_input = self::getValidatedConsumptionInput($ticket_id, $credit_id, $consumed, $credit); + if ($validated_input === false) { + return false; + } + + $input = $validated_input + $input; + $input['users_id'] = $credit->fields['users_id']; + + return $input; + } + /** * Show credit vouchers consumed for a ticket * @@ -205,15 +402,7 @@ public static function showForTicket(Ticket $ticket) return false; } - $canedit = false; - if (Session::haveRight(Entity::$rightname, UPDATE)) { - $canedit = true; // Entity admin has always right to update credits - } elseif ( - $ticket->canEdit($ID) - && !in_array($ticket->fields['status'], array_merge(Ticket::getSolvedStatusArray(), Ticket::getClosedStatusArray())) - ) { - $canedit = true; - } + $canedit = self::canUpdateCreditsForTicket($ticket); $number = self::countForItem($ticket); $rand = mt_rand(); @@ -480,44 +669,13 @@ public static function consumeVoucher(CommonDBTM $item) return; } - $credit_ticket = new self(); - - $credit_entity = new PluginCreditEntity(); - $credit_entity->getFromDB($item->input['plugin_credit_entities_id']); - - $quantity_sold = (int) $credit_entity->fields['quantity']; - $quantity_consumed = $credit_ticket->getConsumedForCreditEntity($item->input['plugin_credit_entities_id']); - $quantity_remaining = max(0, $quantity_sold - $quantity_consumed); - - if (0 !== $quantity_sold && $quantity_remaining < $item->input['plugin_credit_quantity']) { - if ($credit_entity->getField('overconsumption_allowed')) { - Session::addMessageAfterRedirect( - sprintf( - __s('Quantity consumed exceeds remaining credits: %d', 'credit'), - $quantity_remaining, - ), - true, - WARNING, - ); - } else { - Session::addMessageAfterRedirect( - sprintf( - __s('Quantity consumed exceeds remaining credits: %d', 'credit'), - $quantity_remaining, - ), - true, - ERROR, - ); - return; - } - } - $input = [ 'tickets_id' => $ticket->getID(), - 'plugin_credit_entities_id' => $item->input['plugin_credit_entities_id'], - 'consumed' => $item->input['plugin_credit_quantity'], + 'plugin_credit_entities_id' => $item->input['plugin_credit_entities_id'] ?? null, + 'consumed' => $item->input['plugin_credit_quantity'] ?? null, 'users_id' => Session::getLoginUserID(), ]; + $credit_ticket = new self(); if ($credit_ticket->add($input)) { Session::addMessageAfterRedirect( __s('Credit voucher successfully added.', 'credit'), diff --git a/templates/tickets/form.html.twig b/templates/tickets/form.html.twig index 8598738..c9713d4 100644 --- a/templates/tickets/form.html.twig +++ b/templates/tickets/form.html.twig @@ -86,11 +86,15 @@ } if (val == 0) { + quantity.closest('.form-field').classList.add('d-none'); + quantity.disabled = true; + quantity.max = 0; quantity.value = 0; form_help.setAttribute('data-bs-title', __('Maximum consumable quantity', 'credit') + ' : 0 '); form_help.setAttribute('data-bs-content', __('Maximum consumable quantity', 'credit') + ' : 0 '); } else { quantity.closest('.form-field').classList.remove('d-none'); + quantity.disabled = false; $.ajax({ type: 'POST', url: '{{ path('plugins/credit/ajax/dropdownQuantity.php') }}', From a240bfcf0475db6e88e2bbc2d8e4a224a2a1c7b4 Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Fri, 24 Apr 2026 11:55:17 -0500 Subject: [PATCH 2/9] update changelog for credit validation fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c09aae7..27cf6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Improve consumed credits modal readability and open related tickets in a new tab - Show credit vouchers list in entity tab for read-only users while keeping add/config forms restricted to editable contexts. +- Centralize credit consumption validation, prevent invalid voucher selections outside the validity window, and fix the ticket tab quantity field behavior. ## [1.15.2] - 2025-12-22 From 9f28a7f0afc674dc1339a837ef46f81d79c702cd Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Mon, 4 May 2026 09:45:24 -0500 Subject: [PATCH 3/9] address credit validation review --- CHANGELOG.md | 2 +- front/ticket.form.php | 6 +- inc/ticket.class.php | 136 ++++++++++++++++-------------------------- 3 files changed, 52 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cf6e9..39667c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Improve consumed credits modal readability and open related tickets in a new tab - Show credit vouchers list in entity tab for read-only users while keeping add/config forms restricted to editable contexts. -- Centralize credit consumption validation, prevent invalid voucher selections outside the validity window, and fix the ticket tab quantity field behavior. +- Improve credit validation and voucher handling. ## [1.15.2] - 2025-12-22 diff --git a/front/ticket.form.php b/front/ticket.form.php index f72cf86..bb22be9 100644 --- a/front/ticket.form.php +++ b/front/ticket.form.php @@ -31,11 +31,7 @@ use Glpi\Exception\Http\BadRequestHttpException; -Session::checkLoginUser(); - -if (!Session::haveRight('ticket', UPDATE) && !Session::haveRight(Entity::$rightname, UPDATE)) { - throw new BadRequestHttpException(); -} +Session::checkRight(\Ticket::class, UPDATE); $PluginCreditTicket = new PluginCreditTicket(); if (isset($_POST["add"])) { diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 165d6dd..d19394b 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -200,8 +200,50 @@ private static function canUpdateCreditsForTicket(Ticket $ticket): bool && !in_array($ticket->fields['status'], array_merge(Ticket::getSolvedStatusArray(), Ticket::getClosedStatusArray())); } - private static function getValidatedConsumptionInput(int $ticket_id, int $credit_id, int $consumed, ?self $current_credit = null): array|false + private static function checkInput(array $input, ?self $current_credit = null): array|false { + $ticket_id = filter_var( + $input['tickets_id'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($ticket_id === false) { + Session::addMessageAfterRedirect( + __s('Ticket is mandatory.', 'credit'), + true, + ERROR, + ); + return false; + } + + $credit_id = filter_var( + $input['plugin_credit_entities_id'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($credit_id === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher entity must be selected.', 'credit'), + true, + ERROR, + ); + return false; + } + + $consumed = filter_var( + $input['consumed'] ?? null, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 1]], + ); + if ($consumed === false) { + Session::addMessageAfterRedirect( + __s('Credit voucher quantity must be greater than 0.', 'credit'), + true, + ERROR, + ); + return false; + } + $ticket = new Ticket(); if ( !$ticket->getFromDB($ticket_id) @@ -265,49 +307,7 @@ private static function getValidatedConsumptionInput(int $ticket_id, int $credit public function prepareInputForAdd($input) { - $ticket_id = filter_var( - $input['tickets_id'] ?? null, - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($ticket_id === false) { - Session::addMessageAfterRedirect( - __s('Ticket is mandatory.', 'credit'), - true, - ERROR, - ); - return false; - } - - $credit_id = filter_var( - $input['plugin_credit_entities_id'] ?? null, - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($credit_id === false) { - Session::addMessageAfterRedirect( - __s('Credit voucher entity must be selected.', 'credit'), - true, - ERROR, - ); - return false; - } - - $consumed = filter_var( - $input['consumed'] ?? null, - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($consumed === false) { - Session::addMessageAfterRedirect( - __s('Credit voucher quantity must be greater than 0.', 'credit'), - true, - ERROR, - ); - return false; - } - - $validated_input = self::getValidatedConsumptionInput($ticket_id, $credit_id, $consumed); + $validated_input = self::checkInput($input); if ($validated_input === false) { return false; } @@ -334,49 +334,13 @@ public function prepareInputForUpdate($input) return false; } - $ticket_id = filter_var( - $input['tickets_id'] ?? $credit->fields['tickets_id'], - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($ticket_id === false) { - Session::addMessageAfterRedirect( - __s('Ticket is mandatory.', 'credit'), - true, - ERROR, - ); - return false; - } - - $credit_id = filter_var( - $input['plugin_credit_entities_id'] ?? $credit->fields['plugin_credit_entities_id'], - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($credit_id === false) { - Session::addMessageAfterRedirect( - __s('Credit voucher entity must be selected.', 'credit'), - true, - ERROR, - ); - return false; - } - - $consumed = filter_var( - $input['consumed'] ?? $credit->fields['consumed'], - FILTER_VALIDATE_INT, - ['options' => ['min_range' => 1]], - ); - if ($consumed === false) { - Session::addMessageAfterRedirect( - __s('Credit voucher quantity must be greater than 0.', 'credit'), - true, - ERROR, - ); - return false; - } + $input += [ + 'tickets_id' => $credit->fields['tickets_id'], + 'plugin_credit_entities_id' => $credit->fields['plugin_credit_entities_id'], + 'consumed' => $credit->fields['consumed'], + ]; - $validated_input = self::getValidatedConsumptionInput($ticket_id, $credit_id, $consumed, $credit); + $validated_input = self::checkInput($input, $credit); if ($validated_input === false) { return false; } From 9661e81263f78e51aba6666b96ef98d3b77e657f Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Mon, 22 Jun 2026 12:28:23 -0500 Subject: [PATCH 4/9] fix rector ticket class reference --- front/ticket.form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/ticket.form.php b/front/ticket.form.php index bb22be9..a539893 100644 --- a/front/ticket.form.php +++ b/front/ticket.form.php @@ -31,7 +31,7 @@ use Glpi\Exception\Http\BadRequestHttpException; -Session::checkRight(\Ticket::class, UPDATE); +Session::checkRight(Ticket::class, UPDATE); $PluginCreditTicket = new PluginCreditTicket(); if (isset($_POST["add"])) { From 976bf03913944228bb3078213786054866ccea2d Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Tue, 23 Jun 2026 08:03:51 -0500 Subject: [PATCH 5/9] add credit validation tests --- inc/ticket.class.php | 11 ++-- phpunit.xml | 8 +++ tests/TicketTest.php | 131 +++++++++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 32 +++++++++++ 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/TicketTest.php create mode 100644 tests/bootstrap.php diff --git a/inc/ticket.class.php b/inc/ticket.class.php index d19394b..2ff346d 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -200,7 +200,7 @@ private static function canUpdateCreditsForTicket(Ticket $ticket): bool && !in_array($ticket->fields['status'], array_merge(Ticket::getSolvedStatusArray(), Ticket::getClosedStatusArray())); } - private static function checkInput(array $input, ?self $current_credit = null): array|false + private static function checkInput(array $input, ?self $current_credit = null, bool $skip_status_check = false): array|false { $ticket_id = filter_var( $input['tickets_id'] ?? null, @@ -248,7 +248,7 @@ private static function checkInput(array $input, ?self $current_credit = null): if ( !$ticket->getFromDB($ticket_id) || !$ticket->can($ticket_id, READ) - || !self::canUpdateCreditsForTicket($ticket) + || (!$skip_status_check && !self::canUpdateCreditsForTicket($ticket)) ) { Session::addMessageAfterRedirect( __s('You do not have rights to update credit vouchers for this ticket.', 'credit'), @@ -307,13 +307,15 @@ private static function checkInput(array $input, ?self $current_credit = null): public function prepareInputForAdd($input) { - $validated_input = self::checkInput($input); + $skip_status_check = (bool) ($input['_skip_status_check'] ?? false); + unset($input['_skip_status_check']); + + $validated_input = self::checkInput($input, null, $skip_status_check); if ($validated_input === false) { return false; } $input = $validated_input + $input; - $input['users_id'] = (int) Session::getLoginUserID(); return $input; } @@ -638,6 +640,7 @@ public static function consumeVoucher(CommonDBTM $item) 'plugin_credit_entities_id' => $item->input['plugin_credit_entities_id'] ?? null, 'consumed' => $item->input['plugin_credit_quantity'] ?? null, 'users_id' => Session::getLoginUserID(), + '_skip_status_check' => true, ]; $credit_ticket = new self(); if ($credit_ticket->add($input)) { diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9ff185a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests + + + diff --git a/tests/TicketTest.php b/tests/TicketTest.php new file mode 100644 index 0000000..f84ec63 --- /dev/null +++ b/tests/TicketTest.php @@ -0,0 +1,131 @@ +. + * ------------------------------------------------------------------------- + * @author François Legastelois + * @copyright Copyright (C) 2017-2023 by Credit plugin team. + * @license GPLv3 https://www.gnu.org/licenses/gpl-3.0.html + * @link https://github.com/pluginsGLPI/credit + * ------------------------------------------------------------------------- + */ + +use Glpi\Tests\DbTestCase; + +class PluginCreditTicketTest extends DbTestCase +{ + private function createTicket(array $extra = []): Ticket + { + return $this->createItem(Ticket::class, array_merge([ + 'name' => 'Credit test ticket', + 'content' => 'Credit test content', + 'entities_id' => 0, + ], $extra)); + } + + private function createCreditVoucher(array $extra = []): PluginCreditEntity + { + return $this->createItem(PluginCreditEntity::class, array_merge([ + 'name' => 'Credit test voucher', + 'entities_id' => 0, + 'is_active' => 1, + 'quantity' => 10, + 'overconsumption_allowed' => 0, + 'low_credit_alert' => -1, + ], $extra)); + } + + public function testConsumeVoucherOnSolvedTicketFromSolutionHook(): void + { + $this->login('glpi', 'glpi'); + + $tech = new User(); + $this->assertTrue($tech->getFromDBByCrit(['name' => 'tech'])); + + $ticket = $this->createTicket(); + $credit = $this->createCreditVoucher(); + + $this->createItem(Ticket_User::class, [ + 'tickets_id' => $ticket->getID(), + 'users_id' => $tech->getID(), + 'type' => CommonITILActor::ASSIGN, + ]); + + $this->assertTrue($ticket->update([ + 'id' => $ticket->getID(), + 'status' => CommonITILObject::SOLVED, + ])); + + $this->login('tech', 'tech'); + + $solution = new ITILSolution(); + $solution->fields = [ + 'itemtype' => Ticket::class, + 'items_id' => $ticket->getID(), + ]; + $solution->input = [ + 'plugin_credit_consumed_voucher' => 1, + 'plugin_credit_entities_id' => $credit->getID(), + 'plugin_credit_quantity' => 1, + ]; + + PluginCreditTicket::consumeVoucher($solution); + + $this->assertSame(1, countElementsInTable(PluginCreditTicket::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'plugin_credit_entities_id' => $credit->getID(), + 'consumed' => 1, + 'users_id' => $tech->getID(), + ])); + } + + public function testPrepareInputForAddRejectsOutOfWindowVouchers(): void + { + $this->login('glpi', 'glpi'); + + $ticket = $this->createTicket(); + $future_credit = $this->createCreditVoucher([ + 'name' => 'Future credit test voucher', + 'begin_date' => date('Y-m-d', strtotime('+1 day')), + ]); + $expired_credit = $this->createCreditVoucher([ + 'name' => 'Expired credit test voucher', + 'end_date' => date('Y-m-d', strtotime('-1 day')), + ]); + + $credit_ticket = new PluginCreditTicket(); + + $this->assertFalse($credit_ticket->prepareInputForAdd([ + 'tickets_id' => $ticket->getID(), + 'plugin_credit_entities_id' => $future_credit->getID(), + 'consumed' => 1, + 'users_id' => Session::getLoginUserID(), + ])); + + $this->assertFalse($credit_ticket->prepareInputForAdd([ + 'tickets_id' => $ticket->getID(), + 'plugin_credit_entities_id' => $expired_credit->getID(), + 'consumed' => 1, + 'users_id' => Session::getLoginUserID(), + ])); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..711e20f --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,32 @@ +. + * ------------------------------------------------------------------------- + * @author François Legastelois + * @copyright Copyright (C) 2017-2023 by Credit plugin team. + * @license GPLv3 https://www.gnu.org/licenses/gpl-3.0.html + * @link https://github.com/pluginsGLPI/credit + * ------------------------------------------------------------------------- + */ + +$loader = require dirname(__DIR__, 3) . '/vendor/autoload.php'; From 862b9db9623078baf13b207966256ff8668be9bd Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Tue, 23 Jun 2026 08:45:38 -0500 Subject: [PATCH 6/9] fix ticket right class reference --- front/ticket.form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/ticket.form.php b/front/ticket.form.php index a539893..bb22be9 100644 --- a/front/ticket.form.php +++ b/front/ticket.form.php @@ -31,7 +31,7 @@ use Glpi\Exception\Http\BadRequestHttpException; -Session::checkRight(Ticket::class, UPDATE); +Session::checkRight(\Ticket::class, UPDATE); $PluginCreditTicket = new PluginCreditTicket(); if (isset($_POST["add"])) { From ddbf3fd169efdef254deef2f48d49918728a6e14 Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Tue, 23 Jun 2026 08:53:17 -0500 Subject: [PATCH 7/9] fix phpunit bootstrap initialization --- tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 711e20f..acfb553 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,4 +29,4 @@ * ------------------------------------------------------------------------- */ -$loader = require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/tests/bootstrap.php'; From 9970336418c95ac363aae78fd8760508611f39f3 Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Tue, 23 Jun 2026 09:02:06 -0500 Subject: [PATCH 8/9] fix credit date test fixtures --- tests/TicketTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TicketTest.php b/tests/TicketTest.php index f84ec63..1c8ac74 100644 --- a/tests/TicketTest.php +++ b/tests/TicketTest.php @@ -105,11 +105,11 @@ public function testPrepareInputForAddRejectsOutOfWindowVouchers(): void $ticket = $this->createTicket(); $future_credit = $this->createCreditVoucher([ 'name' => 'Future credit test voucher', - 'begin_date' => date('Y-m-d', strtotime('+1 day')), + 'begin_date' => date('Y-m-d 00:00:00', strtotime('+1 day')), ]); $expired_credit = $this->createCreditVoucher([ 'name' => 'Expired credit test voucher', - 'end_date' => date('Y-m-d', strtotime('-1 day')), + 'end_date' => date('Y-m-d 23:59:59', strtotime('-1 day')), ]); $credit_ticket = new PluginCreditTicket(); From bec00c89e19b5a1f1aa54ba8572e371133f72bb7 Mon Sep 17 00:00:00 2001 From: Giovanny Rodriguez Date: Tue, 23 Jun 2026 09:10:09 -0500 Subject: [PATCH 9/9] skip normalized voucher dates in tests --- tests/TicketTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/TicketTest.php b/tests/TicketTest.php index 1c8ac74..d26c05e 100644 --- a/tests/TicketTest.php +++ b/tests/TicketTest.php @@ -44,14 +44,16 @@ private function createTicket(array $extra = []): Ticket private function createCreditVoucher(array $extra = []): PluginCreditEntity { - return $this->createItem(PluginCreditEntity::class, array_merge([ + $input = array_merge([ 'name' => 'Credit test voucher', 'entities_id' => 0, 'is_active' => 1, 'quantity' => 10, 'overconsumption_allowed' => 0, 'low_credit_alert' => -1, - ], $extra)); + ], $extra); + + return $this->createItem(PluginCreditEntity::class, $input, ['begin_date', 'end_date']); } public function testConsumeVoucherOnSolvedTicketFromSolutionHook(): void @@ -105,11 +107,11 @@ public function testPrepareInputForAddRejectsOutOfWindowVouchers(): void $ticket = $this->createTicket(); $future_credit = $this->createCreditVoucher([ 'name' => 'Future credit test voucher', - 'begin_date' => date('Y-m-d 00:00:00', strtotime('+1 day')), + 'begin_date' => date('Y-m-d', strtotime('+1 day')), ]); $expired_credit = $this->createCreditVoucher([ 'name' => 'Expired credit test voucher', - 'end_date' => date('Y-m-d 23:59:59', strtotime('-1 day')), + 'end_date' => date('Y-m-d', strtotime('-1 day')), ]); $credit_ticket = new PluginCreditTicket();