Commit cdfd611d authored by Fernando Andrés Muñoz Bravo's avatar Fernando Andrés Muñoz Bravo Committed by Fernando Andrés Muñoz Bravo
Browse files

Issue #3256780 by waspper, KondratievaS: Prevent auction sniping

parent 2c34079c
Loading
Loading
Loading
Loading
+4 −1
Changes for auctioneer.services.yml: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -2,6 +2,9 @@ services:
  plugin.manager.auctioneer.auction_type:
    class: Drupal\auctioneer\AuctionTypePluginManager
    parent: default_plugin_manager
  auctioneer.auction_field.manager:
    class: Drupal\auctioneer\AuctionFieldManager
    arguments: ['@entity_type.manager']
  auctioneer.auction_type.manager:
    class: Drupal\auctioneer\AuctionTypeManager
    arguments: ['@plugin.manager.auctioneer.auction_type', '@entity_type.manager', '@database', '@logger.factory', '@current_user']
    arguments: ['@plugin.manager.auctioneer.auction_type', '@entity_type.manager', '@auctioneer.auction_field.manager', '@database', '@logger.factory', '@current_user']
+7 −0
Changes for modules/auctioneer_sniper/auctioneer_sniper.info.yml: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
name: Auctioneer - Sniper
type: module
description: 'Auction sniping features for date-ranged auction types.'
package: Auctioneer
core_version_requirement: ^8.8.8 || ^9
dependencies:
  - drupal:auctioneer
+182 −0
Changes for modules/auctioneer_sniper/auctioneer_sniper.module: 182 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Defines the "auctioneer_sniper" primary hooks.
 */

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_ENTITY_TYPE_insert() for entity type "bid".
 */
function auctioneer_sniper_bid_insert(EntityInterface $entity) {
  /** @var \Drupal\auctioneer\Entity\AuctionInterface $auction */
  $auction = $entity->getAuction();
  if ($auction) {
    \Drupal::service('auctioneer_sniper.manager')->requestAuctionLifetimeUpdate($auction);
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter() for "auction_type_form".
 */
function auctioneer_sniper_form_auction_type_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $auction_type = $form_state->getFormObject()->getEntity();
  $form['sniper'] = [
    '#type' => 'details',
    '#title' => t('Sniping options'),
    '#open' => TRUE,
    '#weight' => 10,
    '#tree' => TRUE,
  ];
  if (\Drupal::service('auctioneer_sniper.manager')->auctionTypeIsDateRangeInstance($auction_type)) {
    $sniper = $auction_type->getThirdPartySetting('auctioneer_sniper', 'settings');
    $form['sniper']['enable'] = [
      '#type' => 'checkbox',
      '#title' => t('Enable for this auction type'),
      '#default_value' => !empty($sniper) && $sniper['enable'] ?? FALSE,
    ];
    $form['actions']['submit']['#submit'][] = '_auctioneer_sniper_form_auction_type_form_submit';
  }
  else {
    if ($form_id == 'auction_type_add_form') {
      $message = t('You need to save this auction type before enabling this feature.');
    }
    else {
      $message = t('This auction type does not support sniping features. It does not implement interface <em>@interface</em>.', [
        '@interface' => 'Drupal\auctioneer\Plugin\Auctioneer\AuctionType\DateRangeAuctionTypeInterface',
      ]);
    }
    $form['sniper']['info'] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $message,
    ];
  }
}

/**
 * Custom submit function to set sniping configurations.
 */
function _auctioneer_sniper_form_auction_type_form_submit(array $form, FormStateInterface $form_state) {
  $sniper = $form_state->getValue('sniper');
  $auction_type = $form_state->getFormObject()->getEntity();
  // Saving our configurations.
  $settings = [
    'enable' => (bool) $sniper['enable'],
  ];
  $auction_type->setThirdPartySetting('auctioneer_sniper', 'settings', $settings);
  $auction_type->save();
}

/**
 * Implements hook_ENTITY_TYPE_insert() for entity type "auction_type".
 */
function auctioneer_sniper_auction_type_insert(EntityInterface $entity) {
  auctioneer_sniper_auction_type_post_save($entity);
}

/**
 * Implements hook_ENTITY_TYPE_update() for entity type "auction_type".
 */
function auctioneer_sniper_auction_type_update(EntityInterface $entity) {
  auctioneer_sniper_auction_type_post_save($entity);
}

/**
 * Helper function to provide auction type sniping features.
 */
function auctioneer_sniper_auction_type_post_save(EntityInterface $entity) {
  $settings = $entity->getThirdPartySetting('auctioneer_sniper', 'settings');
  if (!empty($settings) && isset($settings['enable']) && (bool) $settings['enable']) {
    \Drupal::service('auctioneer_sniper.manager')->initializeAuctionTypeSniperConfigurations($entity);
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter() for 'auction_form'.
 */
function auctioneer_sniper_form_auction_form_alter(&$form, FormStateInterface $form_state) {
  $auction = $form_state->getFormObject()->getEntity();
  $auction_type = \Drupal::entityTypeManager()->getStorage('auction_type')->load($auction->bundle());
  $settings = $auction_type->getThirdPartySetting('auctioneer_sniper', 'settings');
  if (
    !\Drupal::service('auctioneer_sniper.manager')->auctionTypeIsDateRangeInstance($auction_type) ||
    empty($settings)
  ) {
    return;
  }
  // Let's tweak form fields for better understanding.
  $groups = [
    'auctioneer_sniper' => [
      'title' => t('Sniping options'),
      'description' => t('Sniping prevention options.'),
      'field_names' => [
        'auction_sniper_control_enable',
      ],
      'field_counts' => 0,
      'weight' => 100,
    ],
    'auctioneer_sniper_threshold' => [
      'title' => t('Threshold'),
      'description' => t('Once auction reaches the remaining time specified here, then auction lifetime will be increased.'),
      'field_names' => [
        'auction_sniper_threshold_amount',
        'auction_sniper_threshold_unit',
      ],
      'field_counts' => 0,
      'group' => 'auctioneer_sniper',
      'weight' => 110,
      'states' => [
        'visible' => [
          ':input[name="auction_sniper_control_enable[value]"]' => ['checked' => TRUE],
        ],
      ],
    ],
    'auctioneer_sniper_increment' => [
      'title' => t('Increment'),
      'description' => t('The time to increment auction lifetime once threshold is reached.'),
      'field_names' => [
        'auction_sniper_increase_amount',
        'auction_sniper_increase_unit',
      ],
      'field_counts' => 0,
      'group' => 'auctioneer_sniper',
      'weight' => 120,
      'states' => [
        'visible' => [
          ':input[name="auction_sniper_control_enable[value]"]' => ['checked' => TRUE],
        ],
      ],
    ],
  ];
  foreach ($groups as $group_name => $group_elements) {
    $form[$group_name] = [
      '#title' => $group_elements['title'],
      '#type' => 'details',
      '#collapsible' => TRUE,
      '#weight' => $group_elements['weight'],
      '#open' => TRUE,
      '#tree' => FALSE,
      '#description' => $group_elements['description'],
    ];
    if (isset($group_elements['group'])) {
      $form[$group_name]['#group'] = $group_elements['group'];
    }
    if (isset($group_elements['states'])) {
      $form[$group_name]['#states'] = $group_elements['states'];
    }
    foreach ($group_elements['field_names'] as $field_name) {
      if (isset($form[$field_name])) {
        $form[$field_name]['#group'] = $group_name;
        // Let's keep count of fields, to know if group should be available.
        $groups[$group_name]['field_counts']++;
      }
    }
    if ($groups[$group_name]['field_counts'] == 0 || !$settings['enable']) {
      $form[$group_name]['#access'] = FALSE;
    }
  }
}
+4 −0
Changes for modules/auctioneer_sniper/auctioneer_sniper.services.yml: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
services:
  auctioneer_sniper.manager:
    class: Drupal\auctioneer_sniper\AuctionSniperManager
    arguments: ['@auctioneer.auction_type.manager']
+202 −0
Changes for modules/auctioneer_sniper/src/AuctionSniperManager.php: 202 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\auctioneer_sniper;

use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\auctioneer\AuctionTypeManagerInterface;
use Drupal\auctioneer\Entity\AuctionInterface;
use Drupal\auctioneer\Entity\AuctionTypeInterface;

/**
 * Provides managing service for auction sniping operations.
 */
class AuctionSniperManager implements AuctionSniperManagerInterface {

  use StringTranslationTrait;

  /**
   * The auction type manager.
   *
   * @var \Drupal\auctioneer\AuctionTypeManagerInterface
   */
  protected $auctionTypeManager;

  /**
   * Constructor method.
   *
   * @param \Drupal\auctioneer\AuctionTypeManagerInterface $auction_type_manager
   *   The auction type manager.
   */
  public function __construct(AuctionTypeManagerInterface $auction_type_manager) {
    $this->auctionTypeManager = $auction_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function initializeAuctionTypeSniperConfigurations(AuctionTypeInterface $auction_type, bool $lock_fields = TRUE) {
    if (!$this->auctionTypeIsDateRangeInstance($auction_type)) {
      // Don't allow initializing settings to the unsupported auction types.
      return;
    }
    $fields = [
      'auction_sniper_control_enable' => BaseFieldDefinition::create('boolean')
        ->setLabel($this->t('Enable sniping prevention'))
        ->setDisplayOptions('form', [
          'type' => 'boolean_checkbox',
          'settings' => [
            'display_label' => TRUE,
          ],
        ])
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayConfigurable('view', TRUE),
      'auction_sniper_threshold_amount' => BaseFieldDefinition::create('integer')
        ->setLabel($this->t('Amount'))
        ->setDisplayOptions('form', [
          'type' => 'number',
        ])
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayConfigurable('view', TRUE),
      'auction_sniper_threshold_unit' => BaseFieldDefinition::create('list_string')
        ->setLabel($this->t('Unit'))
        ->setSettings([
          'allowed_values_function' => '\Drupal\auctioneer_sniper\AuctionSniperManager::getSnipingAllowedDateUnits',
        ])
        ->setDisplayOptions('form', [
          'type' => 'options_select',
        ])
        ->setDisplayConfigurable('view', TRUE)
        ->setDisplayConfigurable('form', TRUE),
      'auction_sniper_increase_amount' => BaseFieldDefinition::create('integer')
        ->setLabel($this->t('Amount'))
        ->setDisplayOptions('form', [
          'type' => 'number',
        ])
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayConfigurable('view', TRUE),
      'auction_sniper_increase_unit' => BaseFieldDefinition::create('list_string')
        ->setLabel($this->t('Unit'))
        ->setSettings([
          'allowed_values_function' => '\Drupal\auctioneer_sniper\AuctionSniperManager::getSnipingAllowedDateUnits',
        ])
        ->setDisplayOptions('form', [
          'type' => 'options_select',
        ])
        ->setDisplayConfigurable('view', TRUE)
        ->setDisplayConfigurable('form', TRUE),
    ];
    $auction_field_manager = $this->auctionTypeManager->getAuctionFieldManager();
    $auction_field_manager->provideEntityTypeFields($fields, 'auction', $auction_type->id(), $lock_fields);
  }

  /**
   * {@inheritdoc}
   */
  public static function getSnipingAllowedDateUnits() : array {
    // Does anyone really need seconds?
    return [
      'minutes' => t('Minutes'),
      'hours' => t('Hours'),
      'days' => t('Days'),
      'weeks' => t('Weeks'),
      'months' => t('Months'),
      'years' => t('Years'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function auctionTypeIsDateRangeInstance(AuctionTypeInterface $auction_type) : bool {
    $bid_type = $auction_type->getBidType();
    $plugin_definition = $bid_type ? $this->auctionTypeManager->getBidTypePluginDefinition($bid_type) : [];
    if (isset($plugin_definition['class'])) {
      $reflection = new \ReflectionClass($plugin_definition['class']);

      return $reflection->implementsInterface('\Drupal\auctioneer\AuctionTypeDateRangeInterface');
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function requestAuctionLifetimeUpdate(AuctionInterface $auction) {
    $auction_type = $auction->getAuctionType();
    if (!$this->auctionTypeIsDateRangeInstance($auction_type)) {
      return;
    }
    if (
      !$auction->hasField('auction_sniper_control_enable') ||
      !$auction->hasField('auction_sniper_threshold_amount') ||
      !$auction->hasField('auction_sniper_threshold_unit') ||
      !$auction->hasField('auction_sniper_increase_amount') ||
      !$auction->hasField('auction_sniper_increase_unit')
    ) {
      return;
    }
    $settings = $auction_type->getThirdPartySetting('auctioneer_sniper', 'settings');
    $enable = (bool) $auction->get('auction_sniper_control_enable')->getString();
    if (!$enable || (!empty($settings) && isset($settings['enable']) && !(bool) $settings['enable'])) {
      return;
    }
    // Only update if entity has this feature enabled.
    $t_amount = intval($auction->get('auction_sniper_threshold_amount')->getString());
    $t_unit = $auction->get('auction_sniper_threshold_unit')->getString();
    $i_amount = intval($auction->get('auction_sniper_increase_amount')->getString());
    $i_unit = $auction->get('auction_sniper_increase_unit')->getString();
    if ($t_amount && $t_unit && $i_amount && $i_unit) {
      $bid_type = $auction_type->getBidType();
      // Our plugin instance.
      $plugin_instance = $this->auctionTypeManager->getAuctionPluginManager()->createInstance($bid_type->getPluginName());
      if ($plugin_instance) {
        $now_date = new DrupalDateTime();
        $auction_dates = $plugin_instance->requestAuctionDates($auction);
        $new_values = [];
        foreach ($auction_dates as $field_name => $date_range_items) {
          if (!$auction->hasField($field_name)) {
            continue;
          }
          $date_range_items = is_array($date_range_items) ? $date_range_items : [$date_range_items];
          $delta = 0;
          foreach ($date_range_items as $date_range_item) {
            if ($date_range_item instanceof DateRangeItem) {
              $start_date = $date_range_item->start_date;
              $end_date = $date_range_item->end_date;
              // Calculated auction dates.
              $t_date = clone $end_date;
              $t_date->modify("-$t_amount $t_unit");
              if ($now_date >= $t_date) {
                $i_date = clone $end_date;
                $i_date->modify("+$i_amount $i_unit");
                // Saving new dates into a temporal array, to update later.
                $new_values[$field_name][$delta] = [
                  'value' => $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
                  'end_value' => $i_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
                ];
              }
              $delta++;
            }
          }
        }
        // Now, let's set updated dates. We support update by delta.
        if (!empty($new_values)) {
          foreach ($new_values as $field_name => $values) {
            foreach ($values as $delta => $value) {
              $auction->{$field_name}[$delta] = $value;
            }
          }
          $auction->save();
        }
      }
    }

    return $auction;
  }

}
Loading