Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

- Credit vouchers can be restricted to incidents and/or requests (both enabled by default)

### Fixed

- Fix access denied error when consuming a credit voucher from the ticket Credit tab
Expand Down
9 changes: 9 additions & 0 deletions front/ticket.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@
throw new AccessDeniedHttpException();
}

if (!PluginCreditEntity::canBeUsedForTicket((int) $_REQUEST['plugin_credit_entities_id'], $ticket)) {
Session::addMessageAfterRedirect(
__s('This credit voucher cannot be used for this type of ticket', 'credit'),
true,
ERROR,
);
Html::back();
}


$input = [
'tickets_id' => $_REQUEST['tickets_id'],
Expand Down
117 changes: 117 additions & 0 deletions inc/entity.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ public static function showForItemtype(Entity $entity, $itemtype = 'Entity')
'quantity_remaining' => __('Quantity remaining', 'credit'),
'overconsumption_allowed' => __('Allow overconsumption', 'credit'),
'low_credit_alert' => __('Low credits alert', 'credit'),
'is_for_incidents' => __('Usable for incidents', 'credit'),
'is_for_requests' => __('Usable for requests', 'credit'),
'entities_id' => __('Entity'),
'is_recursive' => __('Child entities'),
];
Expand Down Expand Up @@ -342,6 +344,22 @@ public function rawSearchOptions()
'unit' => '%',
];

$tab[] = [
'id' => 998,
'table' => self::getTable(),
'field' => 'is_for_incidents',
'name' => __('Usable for incidents', 'credit'),
'datatype' => 'bool',
];

$tab[] = [
'id' => 999,
'table' => self::getTable(),
'field' => 'is_for_requests',
'name' => __('Usable for requests', 'credit'),
'datatype' => 'bool',
];

$tab[] = [
'id' => 80,
'table' => 'glpi_entities',
Expand Down Expand Up @@ -544,6 +562,8 @@ public static function install(Migration $migration)
`quantity` int NOT NULL DEFAULT '0',
`overconsumption_allowed` tinyint NOT NULL DEFAULT '0',
`low_credit_alert` int DEFAULT NULL,
`is_for_incidents` tinyint NOT NULL DEFAULT '1',
`is_for_requests` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `name` (`name`),
KEY `entities_id` (`entities_id`),
Expand All @@ -568,6 +588,10 @@ public static function install(Migration $migration)

// 1.13.2
$migration->addField($table, 'low_credit_alert', 'int', ['update' => "NULL"]);

// Restrict vouchers to incidents and/or requests
$migration->addField($table, 'is_for_incidents', 'bool', ['value' => '1', 'update' => '1']);
$migration->addField($table, 'is_for_requests', 'bool', ['value' => '1', 'update' => '1']);
}

return true;
Expand Down Expand Up @@ -604,6 +628,99 @@ public static function getActiveFilter()
];
}

/**
* Get criteria restricting credit vouchers to those usable for the given ticket type.
*
* @param int $ticket_type Ticket type (Ticket::INCIDENT_TYPE or Ticket::DEMAND_TYPE)
*
* @return array
*/
public static function getTicketTypeFilter(int $ticket_type): array
{
return match ($ticket_type) {
Ticket::INCIDENT_TYPE => ['glpi_plugin_credit_entities.is_for_incidents' => 1],
Ticket::DEMAND_TYPE => ['glpi_plugin_credit_entities.is_for_requests' => 1],
default => [],
};
}

/**
* Get criteria restricting credit vouchers to active ones usable for the given ticket.
*
* @param Ticket $ticket
*
* @return array
*/
public static function getActiveFilterForTicket(Ticket $ticket): array
{
return array_merge(
self::getActiveFilter(),
self::getTicketTypeFilter((int) ($ticket->fields['type'] ?? 0)),
);
}

/**
* Check if at least one active credit voucher can be consumed on the given ticket.
*
* @param Ticket $ticket
*
* @return bool
*/
public static function hasUsableVoucherForTicket(Ticket $ticket): bool
{
return countElementsInTable(self::getTable(), [
'WHERE' => [
getEntitiesRestrictCriteria(self::getTable(), '', $ticket->getEntityID(), true),
self::getActiveFilterForTicket($ticket),
],
]) > 0;
}

/**
* Check if the credit voucher can be used for the given ticket type.
*
* @param int $ticket_type Ticket type (Ticket::INCIDENT_TYPE or Ticket::DEMAND_TYPE)
*
* @return bool
*/
public function isUsableForTicketType(int $ticket_type): bool
{
return match ($ticket_type) {
Ticket::INCIDENT_TYPE => (bool) $this->fields['is_for_incidents'],
Ticket::DEMAND_TYPE => (bool) $this->fields['is_for_requests'],
default => true,
};
}

/**
* Check if the credit voucher can be used for the given ticket, based on the ticket type.
*
* @param int $credit_id ID of the credit voucher
* @param Ticket $ticket
*
* @return bool
*/
public static function canBeUsedForTicket(int $credit_id, Ticket $ticket): bool
{
return self::canBeUsedForTicketType($credit_id, (int) ($ticket->fields['type'] ?? 0));
}

/**
* Check if the credit voucher can be used for the given ticket type.
*
* @param int $credit_id ID of the credit voucher
* @param int $ticket_type Ticket type (Ticket::INCIDENT_TYPE or Ticket::DEMAND_TYPE)
*
* @return bool
*/
public static function canBeUsedForTicketType(int $credit_id, int $ticket_type): bool
{
$credit_entity = new self();

return $credit_entity->getFromDB($credit_id)
&& $credit_entity->isUsableForTicketType($ticket_type);
}

/**
* Check if current user can consume a credit voucher, based on the voucher entity.
*
Expand Down
75 changes: 72 additions & 3 deletions inc/ticket.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public static function showForTicket(Ticket $ticket)
'type_name' => self::getTypeName(2),
'creditentityclass' => PluginCreditEntity::class,
'form_url' => self::getFormUrl(),
'conditions' => PluginCreditEntity::getActiveFilter(),
'conditions' => PluginCreditEntity::getActiveFilterForTicket($ticket),
'canedit' => $canedit,
'ID' => $ID,
'entries' => $entries,
Expand Down Expand Up @@ -327,6 +327,11 @@ public static function displayVoucherInTicketProcessingForm($params)
return;
}

// No voucher can be consumed for this ticket type in this entity
if (!PluginCreditEntity::hasUsableVoucherForTicket($ticket)) {
return;
}

$canedit = $ticket->canEdit($ticket->getID());
if (
in_array($ticket->fields['status'], Ticket::getSolvedStatusArray())
Expand All @@ -351,10 +356,14 @@ public static function displayVoucherInTicketProcessingForm($params)
if ($canedit) {
//get default value for ticket
$default_credit = PluginCreditTicketConfig::getDefaultForTicket($ticket->getID(), $item->getType());
if ($default_credit == 0) {
if ($default_credit == 0 || !PluginCreditEntity::canBeUsedForTicket($default_credit, $ticket)) {
//get default value for entity
$default_credit = PluginCreditEntityConfig::getDefaultForEntityAndType($ticket->getEntityID(), $item->getType());
}
if ($default_credit != 0 && !PluginCreditEntity::canBeUsedForTicket($default_credit, $ticket)) {
// Default voucher cannot be used for this ticket type
$default_credit = 0;
}

if ($default_credit != 0) {
$max = PluginCreditEntity::getMaximumConsumptionForCredit($default_credit);
Expand All @@ -368,7 +377,7 @@ public static function displayVoucherInTicketProcessingForm($params)
'default_credit_max' => $max ?? 0,
'entity_id' => $ticket->getEntityID(),
'type_name' => self::getTypeName(2),
'condition' => PluginCreditEntity::getActiveFilter(),
'condition' => PluginCreditEntity::getActiveFilterForTicket($ticket),
'creditentityclass' => PluginCreditEntity::class,
'plugin_credit_geturl' => plugin_credit_geturl(),
]);
Expand Down Expand Up @@ -496,6 +505,15 @@ public static function consumeVoucher(CommonDBTM $item)
return;
}

if (!$credit_entity->isUsableForTicketType((int) $ticket->fields['type'])) {
Session::addMessageAfterRedirect(
__s('This credit voucher cannot be used for this type of ticket', 'credit'),
true,
ERROR,
);
return;
}

$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);
Expand Down Expand Up @@ -538,6 +556,57 @@ public static function consumeVoucher(CommonDBTM $item)
}
}

public function prepareInputForAdd($input)
{
if (!$this->checkVoucherTicketType($input)) {
return false;
}

return parent::prepareInputForAdd($input);
}

public function prepareInputForUpdate($input)
{
$relation_changed = false;
foreach (['tickets_id', 'plugin_credit_entities_id'] as $field) {
if (array_key_exists($field, $input) && $input[$field] != $this->fields[$field]) {
$relation_changed = true;
}
}

if ($relation_changed && !$this->checkVoucherTicketType($input + $this->fields)) {
return false;
}

return parent::prepareInputForUpdate($input);
}

/**
* Check that the credit voucher can be consumed on the ticket type.
*
* @param array $input Input containing `tickets_id` and `plugin_credit_entities_id` keys
*
* @return bool
*/
private function checkVoucherTicketType(array $input): bool
{
$ticket = new Ticket();
if (!$ticket->getFromDB((int) ($input['tickets_id'] ?? 0))) {
return true;
}

if (PluginCreditEntity::canBeUsedForTicket((int) ($input['plugin_credit_entities_id'] ?? 0), $ticket)) {
return true;
}

Session::addMessageAfterRedirect(
__s('This credit voucher cannot be used for this type of ticket', 'credit'),
true,
ERROR,
);
return false;
}

public function rawSearchOptions()
{
$tab = parent::rawSearchOptions();
Expand Down
57 changes: 56 additions & 1 deletion inc/ticketconfig.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,67 @@ public static function showForTicket(Ticket $ticket, bool $embed_in_ticket_form
'uncollapsed' => $uncollapsed ?? false,
'type_name' => self::getTypeName(),
'creditentitytype' => PluginCreditEntity::class,
'condition' => PluginCreditEntity::getActiveFilter(),
'condition' => PluginCreditEntity::getActiveFilterForTicket($ticket),
'entity_id' => $ticket->getEntityID(),
'ticket' => $ticket,
'ticket_config' => $ticket_config,
'form_url' => $form_url ?? '',
]);
}

public function prepareInputForAdd($input)
{
return parent::prepareInputForAdd($this->removeUnusableDefaults($input));
}

public function prepareInputForUpdate($input)
{
return parent::prepareInputForUpdate($this->removeUnusableDefaults($input));
}

/**
* Reset default vouchers that cannot be used for the ticket type.
*
* @param array $input
*
* @return array
*/
private function removeUnusableDefaults(array $input): array
{
if (array_key_exists('_ticket_type', $input)) {
$ticket_type = (int) $input['_ticket_type'];
} else {
$ticket = new Ticket();
$tickets_id = $input['tickets_id'] ?? $this->fields['tickets_id'] ?? 0;
if (!$ticket->getFromDB((int) $tickets_id)) {
return $input;
}
$ticket_type = (int) $ticket->fields['type'];
}

$config_fields = [
'plugin_credit_entities_id_followups',
'plugin_credit_entities_id_tasks',
'plugin_credit_entities_id_solutions',
];
foreach ($config_fields as $field) {
if (
array_key_exists($field, $input)
&& (int) $input[$field] !== 0
&& !PluginCreditEntity::canBeUsedForTicketType((int) $input[$field], $ticket_type)
) {
$input[$field] = 0;
Session::addMessageAfterRedirect(
__s('This credit voucher cannot be used for this type of ticket', 'credit'),
true,
WARNING,
);
}
}

return $input;
}

public static function updateConfig(Ticket $ticket)
{
if (!Session::haveRight(self::$rightname, self::TICKET_FORM)) {
Expand All @@ -156,6 +209,8 @@ public static function updateConfig(Ticket $ticket)
return;
}
$input['tickets_id'] = $ticket->getID();
// Ticket type may be changed in the same form, and is not yet stored in DB on `pre_item_update`
$input['_ticket_type'] = $ticket->input['type'] ?? $ticket->fields['type'] ?? 0;

$ticket_config = new self();
if ($ticket_config->getFromDBByCrit(['tickets_id' => $ticket->getID()])) {
Expand Down
Loading