Skip to content

Commit 070dde5

Browse files
committed
Issue #2980713 by bojanz: Switch order item adjustments from per-unit to per-line rounding
1 parent e87b4ac commit 070dde5

16 files changed

Lines changed: 342 additions & 144 deletions

File tree

modules/order/commerce_order.install

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,20 @@ function commerce_order_update_8203() {
5656
$update_manager = \Drupal::entityDefinitionUpdateManager();
5757
$update_manager->installFieldStorageDefinition('locked', 'commerce_order', 'commerce_order', $storage_definition);
5858
}
59+
60+
/**
61+
* Add the 'uses_legacy_adjustments' field to 'commerce_order_item' entities.
62+
*/
63+
function commerce_order_update_8204() {
64+
$storage_definition = BaseFieldDefinition::create('boolean')
65+
->setLabel(t('Uses legacy adjustments'))
66+
->setSettings([
67+
'on_label' => t('Yes'),
68+
'off_label' => t('No'),
69+
])
70+
->setDefaultValue(FALSE)
71+
->setInitialValue(TRUE);
72+
73+
$update_manager = \Drupal::entityDefinitionUpdateManager();
74+
$update_manager->installFieldStorageDefinition('uses_legacy_adjustments', 'commerce_order_item', 'commerce_order', $storage_definition);
75+
}

modules/order/src/Entity/Order.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,15 @@ public function clearAdjustments() {
306306
};
307307
// Remove all unlocked adjustments.
308308
foreach ($this->getItems() as $order_item) {
309+
/** @var \Drupal\commerce_order\Adjustment[] $adjustments */
309310
$adjustments = array_filter($order_item->getAdjustments(), $locked_callback);
311+
// Convert legacy locked adjustments.
312+
if ($adjustments && $order_item->usesLegacyAdjustments()) {
313+
foreach ($adjustments as $index => $adjustment) {
314+
$adjustments[$index] = $adjustment->multiply($order_item->getQuantity());
315+
}
316+
}
317+
$order_item->set('uses_legacy_adjustments', FALSE);
310318
$order_item->setAdjustments($adjustments);
311319
}
312320
$adjustments = array_filter($this->getAdjustments(), $locked_callback);
@@ -322,9 +330,10 @@ public function collectAdjustments() {
322330
$adjustments = [];
323331
foreach ($this->getItems() as $order_item) {
324332
foreach ($order_item->getAdjustments() as $adjustment) {
325-
// Order item adjustments apply to the unit price, they
326-
// must be multiplied by quantity before they are used.
327-
$adjustments[] = $adjustment->multiply($order_item->getQuantity());
333+
if ($order_item->usesLegacyAdjustments()) {
334+
$adjustment = $adjustment->multiply($order_item->getQuantity());
335+
}
336+
$adjustments[] = $adjustment;
328337
}
329338
}
330339
foreach ($this->getAdjustments() as $adjustment) {

modules/order/src/Entity/OrderInterface.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,7 @@ public function clearAdjustments();
234234
* Collects all adjustments that belong to the order.
235235
*
236236
* Unlike getAdjustments() which returns only order adjustments, this
237-
* method returns both order and order item adjustments (multiplied
238-
* by quantity).
237+
* method returns both order and order item adjustments.
239238
*
240239
* Important:
241240
* The returned adjustments are unprocessed, and must be processed before use.

modules/order/src/Entity/OrderItem.php

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -144,21 +144,9 @@ public function isUnitPriceOverridden() {
144144
/**
145145
* {@inheritdoc}
146146
*/
147-
public function getAdjustedUnitPrice(array $adjustment_types = []) {
148-
if ($unit_price = $this->getUnitPrice()) {
149-
$adjusted_price = $unit_price;
150-
foreach ($this->getAdjustments() as $adjustment) {
151-
if ($adjustment_types && !in_array($adjustment->getType(), $adjustment_types)) {
152-
continue;
153-
}
154-
if ($adjustment->isIncluded()) {
155-
continue;
156-
}
157-
158-
$adjusted_price = $adjusted_price->add($adjustment->getAmount());
159-
}
160-
161-
return $adjusted_price;
147+
public function getTotalPrice() {
148+
if (!$this->get('total_price')->isEmpty()) {
149+
return $this->get('total_price')->first()->toPrice();
162150
}
163151
}
164152

@@ -196,22 +184,82 @@ public function removeAdjustment(Adjustment $adjustment) {
196184
/**
197185
* {@inheritdoc}
198186
*/
199-
public function getTotalPrice() {
200-
if (!$this->get('total_price')->isEmpty()) {
201-
return $this->get('total_price')->first()->toPrice();
202-
}
187+
public function usesLegacyAdjustments() {
188+
return $this->get('uses_legacy_adjustments')->value;
203189
}
204190

205191
/**
206192
* {@inheritdoc}
207193
*/
208194
public function getAdjustedTotalPrice(array $adjustment_types = []) {
209-
if ($adjusted_unit_price = $this->getAdjustedUnitPrice($adjustment_types)) {
210-
$rounder = \Drupal::service('commerce_price.rounder');
195+
$total_price = $this->getTotalPrice();
196+
if (!$total_price) {
197+
return NULL;
198+
}
199+
200+
if ($this->usesLegacyAdjustments()) {
201+
$adjusted_unit_price = $this->getAdjustedUnitPrice($adjustment_types);
211202
$adjusted_total_price = $adjusted_unit_price->multiply($this->getQuantity());
212-
$adjusted_total_price = $rounder->round($adjusted_total_price);
213-
return $adjusted_total_price;
214203
}
204+
else {
205+
$adjusted_total_price = $this->applyAdjustments($total_price, $adjustment_types);
206+
}
207+
208+
$rounder = \Drupal::service('commerce_price.rounder');
209+
$adjusted_total_price = $rounder->round($adjusted_total_price);
210+
211+
return $adjusted_total_price;
212+
}
213+
214+
/**
215+
* {@inheritdoc}
216+
*/
217+
public function getAdjustedUnitPrice(array $adjustment_types = []) {
218+
$unit_price = $this->getUnitPrice();
219+
if (!$unit_price) {
220+
return NULL;
221+
}
222+
223+
if ($this->usesLegacyAdjustments()) {
224+
$adjusted_unit_price = $this->applyAdjustments($unit_price, $adjustment_types);
225+
}
226+
else {
227+
$adjusted_total_price = $this->getAdjustedTotalPrice($adjustment_types);
228+
$adjusted_unit_price = $adjusted_total_price->divide($this->getQuantity());
229+
}
230+
231+
$rounder = \Drupal::service('commerce_price.rounder');
232+
$adjusted_unit_price = $rounder->round($adjusted_unit_price);
233+
234+
return $adjusted_unit_price;
235+
}
236+
237+
/**
238+
* Applies adjustments to the given price.
239+
*
240+
* @param \Drupal\commerce_price\Price $price
241+
* The price.
242+
* @param string[] $adjustment_types
243+
* The adjustment types to include in the adjusted price.
244+
* Examples: fee, promotion, tax. Defaults to all adjustment types.
245+
*
246+
* @return \Drupal\commerce_price\Price
247+
* The adjusted price.
248+
*/
249+
protected function applyAdjustments(Price $price, array $adjustment_types = []) {
250+
$adjusted_price = $price;
251+
foreach ($this->getAdjustments() as $adjustment) {
252+
if ($adjustment_types && !in_array($adjustment->getType(), $adjustment_types)) {
253+
continue;
254+
}
255+
if ($adjustment->isIncluded()) {
256+
continue;
257+
}
258+
259+
$adjusted_price = $adjusted_price->add($adjustment->getAmount());
260+
}
261+
262+
return $adjusted_price;
215263
}
216264

217265
/**
@@ -337,19 +385,27 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
337385
->setDescription(t('Whether the unit price is overridden.'))
338386
->setDefaultValue(FALSE);
339387

388+
$fields['total_price'] = BaseFieldDefinition::create('commerce_price')
389+
->setLabel(t('Total price'))
390+
->setDescription(t('The total price of the order item.'))
391+
->setReadOnly(TRUE)
392+
->setDisplayConfigurable('form', FALSE)
393+
->setDisplayConfigurable('view', TRUE);
394+
340395
$fields['adjustments'] = BaseFieldDefinition::create('commerce_adjustment')
341396
->setLabel(t('Adjustments'))
342397
->setRequired(FALSE)
343398
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
344399
->setDisplayConfigurable('form', FALSE)
345400
->setDisplayConfigurable('view', TRUE);
346401

347-
$fields['total_price'] = BaseFieldDefinition::create('commerce_price')
348-
->setLabel(t('Total price'))
349-
->setDescription(t('The total price of the order item.'))
350-
->setReadOnly(TRUE)
351-
->setDisplayConfigurable('form', FALSE)
352-
->setDisplayConfigurable('view', TRUE);
402+
$fields['uses_legacy_adjustments'] = BaseFieldDefinition::create('boolean')
403+
->setLabel(t('Uses legacy adjustments'))
404+
->setSettings([
405+
'on_label' => t('Yes'),
406+
'off_label' => t('No'),
407+
])
408+
->setDefaultValue(FALSE);
353409

354410
$fields['data'] = BaseFieldDefinition::create('map')
355411
->setLabel(t('Data'))

modules/order/src/Entity/OrderItemInterface.php

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -119,36 +119,29 @@ public function setUnitPrice(Price $unit_price, $override = FALSE);
119119
public function isUnitPriceOverridden();
120120

121121
/**
122-
* Gets the adjusted order item unit price.
123-
*
124-
* The adjusted unit price is calculated by applying the order item's
125-
* adjustments to the unit price. This can include promotions, taxes, etc.
126-
*
127-
* Adjustments are usually included only in the order total price, but
128-
* knowing the adjusted unit prices for each order item can be useful for
129-
* refunds and other purposes.
130-
*
131-
* @param string[] $adjustment_types
132-
* The adjustment types to include in the adjusted price.
133-
* Examples: fee, promotion, tax. Defaults to all adjustment types.
122+
* Gets the order item total price.
134123
*
135124
* @return \Drupal\commerce_price\Price|null
136-
* The adjusted order item unit price, or NULL.
125+
* The order item total price, or NULL.
137126
*/
138-
public function getAdjustedUnitPrice(array $adjustment_types = []);
127+
public function getTotalPrice();
139128

140129
/**
141-
* Gets the order item total price.
130+
* Gets whether the order item uses legacy adjustments.
142131
*
143-
* @return \Drupal\commerce_price\Price|null
144-
* The order item total price, or NULL.
132+
* Indicates that the adjustments were calculated based on the unit price,
133+
* which was the default logic prior to Commerce 2.8, changed in #2980713.
134+
*
135+
* @return bool
136+
* TRUE if the order item uses legacy adjustments, FALSE otherwise.
145137
*/
146-
public function getTotalPrice();
138+
public function usesLegacyAdjustments();
147139

148140
/**
149141
* Gets the adjusted order item total price.
150142
*
151-
* Calculated by multiplying the adjusted unit price by quantity.
143+
* The adjusted total price is calculated by applying the order item's
144+
* adjustments to the total price. This can include promotions, taxes, etc.
152145
*
153146
* @param string[] $adjustment_types
154147
* The adjustment types to include in the adjusted price.
@@ -159,6 +152,23 @@ public function getTotalPrice();
159152
*/
160153
public function getAdjustedTotalPrice(array $adjustment_types = []);
161154

155+
/**
156+
* Gets the adjusted order item unit price.
157+
*
158+
* Calculated by dividing the adjusted total price by quantity.
159+
*
160+
* Useful for refunds and other purposes where there's a need to know
161+
* how much a single unit contributed to the order total.
162+
*
163+
* @param string[] $adjustment_types
164+
* The adjustment types to include in the adjusted price.
165+
* Examples: fee, promotion, tax. Defaults to all adjustment types.
166+
*
167+
* @return \Drupal\commerce_price\Price|null
168+
* The adjusted order item unit price, or NULL.
169+
*/
170+
public function getAdjustedUnitPrice(array $adjustment_types = []);
171+
162172
/**
163173
* Gets an order item data value with the given key.
164174
*

modules/order/src/EntityAdjustableInterface.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@
77
/**
88
* Defines an interface for objects that contain adjustments.
99
*
10+
* Adjustments store promotions, taxes, fees, shipping costs.
11+
* They can be calculated on the order level (based on the order subtotal),
12+
* or on the order item level (based on the order item total).
13+
*
14+
* if $order_item->usesLegacyAdjustments() is true, the order item adjustments
15+
* were calculated based on the order item unit price, which was the default
16+
* logic prior to Commerce 2.8, changed in #2980713.
17+
*
18+
* Adjustments are always displayed in the order total summary, below
19+
* the subtotal. They are not shown as a part of the order item prices.
20+
* To get the order item total price with adjustments included, use
21+
* $order_item->getAdjustedTotalPrice().
22+
*
1023
* @see \Drupal\commerce_order\Entity\OrderInterfaceEntity
1124
* @see \Drupal\commerce_order\Entity\OrderItemInterfaceEntity
1225
*/

modules/order/src/PriceCalculator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public function calculate(PurchasableEntityInterface $purchasable_entity, $quant
126126
foreach ($processors as $processor) {
127127
$processor->process($order);
128128
}
129-
$calculated_price = $order_item->getAdjustedUnitPrice();
129+
$calculated_price = $order_item->getAdjustedTotalPrice();
130130
$adjustments = $order_item->getAdjustments();
131131
$adjustments = $this->adjustmentTransformer->processAdjustments($adjustments);
132132

0 commit comments

Comments
 (0)