Commit 2b266e7a authored by Bojan Živanović's avatar Bojan Živanović
Browse files

Issue #2943961: Create a AdjustmentTransformer service for combining/sorting/rounding adjustments

parent d87e374b
Loading
Loading
Loading
Loading
+10 −6
Original line number Diff line number Diff line
services:
  plugin.manager.commerce_adjustment_type:
    class: Drupal\commerce_order\AdjustmentTypeManager
    arguments: ['@module_handler', '@cache.discovery']

  commerce_order.chain_order_type_resolver:
    class: Drupal\commerce_order\Resolver\ChainOrderTypeResolver
    tags:
@@ -10,6 +14,10 @@ services:
    tags:
      - { name: commerce_order.order_type_resolver, priority: -100 }

  commerce_order.adjustment_transformer:
    class: Drupal\commerce_order\AdjustmentTransformer
    arguments: ['@plugin.manager.commerce_adjustment_type', '@commerce_price.rounder']

  commerce_order.order_assignment:
    class: Drupal\commerce_order\OrderAssignment
    arguments: ['@entity_type.manager', '@event_dispatcher']
@@ -49,13 +57,9 @@ services:
    tags:
      - { name: 'event_subscriber' }

  plugin.manager.commerce_adjustment_type:
    class: Drupal\commerce_order\AdjustmentTypeManager
    arguments: ['@module_handler', '@cache.discovery']

  commerce_order.order_total_summary:
    class: Drupal\commerce_order\OrderTotalSummary
    arguments: ['@plugin.manager.commerce_adjustment_type']
    arguments: ['@commerce_order.adjustment_transformer']

  commerce_order.order_store_resolver:
    class: Drupal\commerce_order\Resolver\OrderStoreResolver
@@ -65,4 +69,4 @@ services:

  commerce_order.price_calculator:
    class: Drupal\commerce_order\PriceCalculator
    arguments: ['@entity_type.manager', '@commerce_price.chain_price_resolver', '@commerce_order.chain_order_type_resolver', '@request_stack']
    arguments: ['@commerce_order.adjustment_transformer', '@commerce_order.chain_order_type_resolver', '@commerce_price.chain_price_resolver', '@entity_type.manager', '@request_stack']
+120 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\commerce_order;

use Drupal\commerce_price\RounderInterface;
use Drupal\Component\Utility\SortArray;

class AdjustmentTransformer implements AdjustmentTransformerInterface {

  /**
   * The adjustment type manager.
   *
   * @var \Drupal\commerce_order\AdjustmentTypeManager
   */
  protected $adjustmentTypeManager;

  /**
   * The rounder.
   *
   * @var \Drupal\commerce_price\RounderInterface
   */
  protected $rounder;

  /**
   * Constructs a new AdjustmentTransformer object.
   *
   * @param \Drupal\commerce_order\AdjustmentTypeManager $adjustment_type_manager
   *   The adjustment type manager.
   * @param \Drupal\commerce_price\RounderInterface $rounder
   *   The rounder.
   */
  public function __construct(AdjustmentTypeManager $adjustment_type_manager, RounderInterface $rounder) {
    $this->adjustmentTypeManager = $adjustment_type_manager;
    $this->rounder = $rounder;
  }

  /**
   * {@inheritdoc}
   */
  public function processAdjustments(array $adjustments) {
    $adjustments = $this->combineAdjustments($adjustments);
    $adjustments = $this->sortAdjustments($adjustments);
    $adjustments = $this->roundAdjustments($adjustments);

    return $adjustments;
  }

  /**
   * {@inheritdoc}
   */
  public function combineAdjustments(array $adjustments) {
    $combined_adjustments = [];
    foreach ($adjustments as $index => $adjustment) {
      $type = $adjustment->getType();
      $source_id = $adjustment->getSourceId();
      if (empty($source_id)) {
        // Adjustments without a source ID are always shown standalone.
        $key = $index;
      }
      else {
        // Adjustments with the same type and source ID are combined.
        $key = $type . '_' . $source_id;
      }

      if (empty($combined_adjustments[$key])) {
        $combined_adjustments[$key] = $adjustment;
      }
      else {
        $combined_adjustments[$key] = $combined_adjustments[$key]->add($adjustment);
      }
    }
    // The keys used for combining are irrelevant to the caller.
    $combined_adjustments = array_values($combined_adjustments);

    return $combined_adjustments;
  }

  /**
   * {@inheritdoc}
   */
  public function sortAdjustments(array $adjustments) {
    $types = $this->adjustmentTypeManager->getDefinitions();
    $data = [];
    foreach ($adjustments as $adjustment) {
      $data[] = [
        'adjustment' => $adjustment,
        'weight' => $types[$adjustment->getType()]['weight'],
      ];
    }
    uasort($data, [SortArray::class, 'sortByWeightElement']);
    // Re-extract the adjustments from the sorted array.
    $adjustments = array_column($data, 'adjustment');

    return $adjustments;
  }

  /**
   * {@inheritdoc}
   */
  public function roundAdjustments(array $adjustments, $mode = PHP_ROUND_HALF_UP) {
    foreach ($adjustments as $index => $adjustment) {
      $adjustments[$index] = $this->roundAdjustment($adjustment, $mode);
    }

    return $adjustments;
  }

  /**
   * {@inheritdoc}
   */
  public function roundAdjustment(Adjustment $adjustment, $mode = PHP_ROUND_HALF_UP) {
    $amount = $this->rounder->round($adjustment->getAmount(), $mode);
    $adjustment = new Adjustment([
      'amount' => $amount,
    ] + $adjustment->toArray());

    return $adjustment;
  }

}
+83 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\commerce_order;

/**
 * Provides common logic for processing and transforming adjustments.
 */
interface AdjustmentTransformerInterface {

  /**
   * Combines, sorts, and rounds the given adjustments.
   *
   * @param \Drupal\commerce_order\Adjustment[] $adjustments
   *   The adjustments.
   *
   * @return \Drupal\commerce_order\Adjustment[]
   *   The processed adjustments.
   */
  public function processAdjustments(array $adjustments);

  /**
   * Combines adjustments with the same type and source ID.
   *
   * For example, all tax adjustments generated by the same tax type
   * will be combined into a single adjustment, which can then be shown
   * in total summaries.
   *
   * @param \Drupal\commerce_order\Adjustment[] $adjustments
   *   The adjustments.
   *
   * @return \Drupal\commerce_order\Adjustment[]
   *   The combined adjustments.
   */
  public function combineAdjustments(array $adjustments);

  /**
   * Sorts adjustments by their type's weight.
   *
   * For example, tax adjustments will be placed after promotion adjustments,
   * because the tax adjustment type has a higher weight than the promotion
   * one, as defined in commerce_order.commerce_adjustment_types.yml.
   *
   * @param \Drupal\commerce_order\Adjustment[] $adjustments
   *   The adjustments.
   *
   * @return \Drupal\commerce_order\Adjustment[]
   *   The sorted adjustments.
   */
  public function sortAdjustments(array $adjustments);

  /**
   * Rounds adjustments to their currency precision.
   *
   * For example, USD adjustments will be rounded to 2 decimals.
   *
   * @param \Drupal\commerce_order\Adjustment[] $adjustments
   *   The adjustments.
   * @param int $mode
   *   The rounding mode. One of the following constants: PHP_ROUND_HALF_UP,
   *   PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD.
   *
   * @return \Drupal\commerce_order\Adjustment[]
   *   The rounded adjustments.
   */
  public function roundAdjustments(array $adjustments, $mode = PHP_ROUND_HALF_UP);

  /**
   * Rounds an adjustment to its currency precision.
   *
   * For example, a USD adjustment will be rounded to 2 decimals.
   *
   * @param \Drupal\commerce_order\Adjustment $adjustment
   *   The adjustment.
   * @param int $mode
   *   The rounding mode. One of the following constants: PHP_ROUND_HALF_UP,
   *   PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD.
   *
   * @return \Drupal\commerce_order\Adjustment
   *   The rounded adjustment.
   */
  public function roundAdjustment(Adjustment $adjustment, $mode = PHP_ROUND_HALF_UP);

}
+6 −4
Original line number Diff line number Diff line
@@ -233,12 +233,14 @@ interface OrderInterface extends ContentEntityInterface, EntityAdjustableInterfa
  /**
   * Collects all adjustments that belong to the order.
   *
   * Unlike getAdjustments() which returns only order adjustments,
   * this method returns both order and order item adjustments.
   * Unlike getAdjustments() which returns only order adjustments, this
   * method returns both order and order item adjustments (multiplied
   * by quantity).
   *
   * Important:
   * The returned order item adjustments are multiplied by quantity,
   * so that they can be safely added to the order adjustments.
   * The returned adjustments are unprocessed, and must be processed before use.
   *
   * @see \Drupal\commerce_order\AdjustmentTransformerInterface::processAdjustments()
   *
   * @return \Drupal\commerce_order\Adjustment[]
   *   The adjustments.
+18 −35
Original line number Diff line number Diff line
@@ -3,57 +3,40 @@
namespace Drupal\commerce_order;

use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Component\Utility\SortArray;

class OrderTotalSummary implements OrderTotalSummaryInterface {

  /**
   * The adjustment type manager.
   * The adjustment transformer.
   *
   * @var \Drupal\commerce_order\AdjustmentTypeManager
   * @var \Drupal\commerce_order\AdjustmentTransformerInterface
   */
  protected $adjustmentTypeManager;
  protected $adjustmentTransformer;

  /**
   * {@inheritdoc}
   * Constructs a new OrderTotalSummary object.
   *
   * @param \Drupal\commerce_order\AdjustmentTransformerInterface $adjustment_transformer
   *   The adjustment transformer.
   */
  public function __construct(AdjustmentTypeManager $adjustment_type_manager) {
    $this->adjustmentTypeManager = $adjustment_type_manager;
  public function __construct(AdjustmentTransformerInterface $adjustment_transformer) {
    $this->adjustmentTransformer = $adjustment_transformer;
  }

  /**
   * {@inheritdoc}
   */
  public function buildTotals(OrderInterface $order) {
    $types = $this->adjustmentTypeManager->getDefinitions();
    $adjustments = [];
    foreach ($order->collectAdjustments() as $adjustment) {
      $type = $adjustment->getType();
      $source_id = $adjustment->getSourceId();
      if (empty($source_id)) {
        // Adjustments without a source ID are always shown standalone.
        $key = count($adjustments);
      }
      else {
        // Adjustments with the same type and source ID are combined.
        $key = $type . '_' . $source_id;
      }

      if (empty($adjustments[$key])) {
        $adjustments[$key] = [
          'type' => $type,
          'label' => $adjustment->getLabel(),
          'total' => $adjustment->getAmount(),
          'percentage' => $adjustment->getPercentage(),
          'weight' => $types[$type]['weight'],
        ];
      }
      else {
        $adjustments[$key]['total'] = $adjustments[$key]['total']->add($adjustment->getAmount());
      }
    $adjustments = $order->collectAdjustments();
    $adjustments = $this->adjustmentTransformer->processAdjustments($adjustments);
    // Convert the adjustments to arrays.
    $adjustments = array_map(function (Adjustment $adjustment) {
      return $adjustment->toArray();
    }, $adjustments);
    // Provide the "total" key for backwards compatibility reasons.
    foreach ($adjustments as $index => $adjustment) {
      $adjustments[$index]['total'] = $adjustments[$index]['amount'];
    }
    // Sort the adjustments by weight.
    uasort($adjustments, [SortArray::class, 'sortByWeightElement']);

    return [
      'subtotal' => $order->getSubtotalPrice(),
Loading