Skip to content

Commit 0a8c85b

Browse files
Gralothbojanz
authored andcommitted
Issue #2915163 by Dionsj, lisastreeter, bojanz: Order Item fields not taken into account when merging
1 parent 2e2b788 commit 0a8c85b

8 files changed

Lines changed: 445 additions & 4 deletions

modules/cart/commerce_cart.services.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ services:
1919

2020
commerce_cart.order_item_matcher:
2121
class: Drupal\commerce_cart\OrderItemMatcher
22-
arguments: ['@event_dispatcher']
22+
arguments: ['@entity_type.manager', '@event_dispatcher']
2323

2424
commerce_cart.cart_subscriber:
2525
class: Drupal\commerce_cart\EventSubscriber\CartEventSubscriber

modules/cart/src/OrderItemMatcher.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@
55
use Drupal\commerce_cart\Event\CartEvents;
66
use Drupal\commerce_cart\Event\OrderItemComparisonFieldsEvent;
77
use Drupal\commerce_order\Entity\OrderItemInterface;
8+
use Drupal\Core\Entity\EntityTypeManagerInterface;
89
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
910

1011
/**
1112
* Default implementation of the order item matcher.
1213
*/
1314
class OrderItemMatcher implements OrderItemMatcherInterface {
1415

16+
/**
17+
* The entity type manager.
18+
*
19+
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
20+
*/
21+
protected $entityTypeManager;
22+
1523
/**
1624
* The event dispatcher.
1725
*
@@ -22,10 +30,13 @@ class OrderItemMatcher implements OrderItemMatcherInterface {
2230
/**
2331
* Constructs a new OrderItemMatcher object.
2432
*
33+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
34+
* The entity type manager.
2535
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
2636
* The event dispatcher.
2737
*/
28-
public function __construct(EventDispatcherInterface $event_dispatcher) {
38+
public function __construct(EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher) {
39+
$this->entityTypeManager = $entity_type_manager;
2940
$this->eventDispatcher = $event_dispatcher;
3041
}
3142

@@ -48,9 +59,11 @@ public function matchAll(OrderItemInterface $order_item, array $order_items) {
4859
}
4960

5061
$comparison_fields = ['type', 'purchased_entity'];
62+
$comparison_fields = array_merge($comparison_fields, $this->getCustomFields($order_item));
5163
$event = new OrderItemComparisonFieldsEvent($comparison_fields, $order_item);
5264
$this->eventDispatcher->dispatch(CartEvents::ORDER_ITEM_COMPARISON_FIELDS, $event);
5365
$comparison_fields = $event->getComparisonFields();
66+
$comparison_fields = array_unique($comparison_fields);
5467

5568
$matched_order_items = [];
5669
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $existing_order_item */
@@ -60,7 +73,7 @@ public function matchAll(OrderItemInterface $order_item, array $order_items) {
6073
// The field is missing on one of the order items.
6174
continue 2;
6275
}
63-
if ($existing_order_item->get($comparison_field)->getValue() !== $order_item->get($comparison_field)->getValue()) {
76+
if (!$existing_order_item->get($comparison_field)->equals($order_item->get($comparison_field))) {
6477
// Order item doesn't match.
6578
continue 2;
6679
}
@@ -71,4 +84,24 @@ public function matchAll(OrderItemInterface $order_item, array $order_items) {
7184
return $matched_order_items;
7285
}
7386

87+
/**
88+
* Gets the names of custom fields shown on the add to cart form.
89+
*
90+
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
91+
* The order item.
92+
*
93+
* @return string[]
94+
* The field names.
95+
*/
96+
protected function getCustomFields(OrderItemInterface $order_item) {
97+
$storage = $this->entityTypeManager->getStorage('entity_form_display');
98+
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
99+
$form_display = $storage->load('commerce_order_item.' . $order_item->bundle() . '.' . 'add_to_cart');
100+
$field_names = array_keys($form_display->getComponents());
101+
// Remove base fields.
102+
$field_names = array_diff($field_names, ['purchased_entity', 'quantity', 'created']);
103+
104+
return $field_names;
105+
}
106+
74107
}

modules/cart/src/OrderItemMatcherInterface.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88
* Finds matching order items.
99
*
1010
* Used for combining order items in the add to cart process.
11+
*
12+
* By default, takes into account the order item type, purchased entity ID,
13+
* and any custom fields shown on the add to cart form.
14+
*
15+
* For example, when a custom "engraving" field is shown on the add to cart
16+
* form, two order items will be combined if they have the same
17+
* engraving, type, purchased entity ID.
1118
*/
1219
interface OrderItemMatcherInterface {
1320

1421
/**
15-
* Finds the best matching order item for the given order item.
22+
* Finds the first matching order item for the given order item.
1623
*
1724
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
1825
* The order item.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
langcode: en
2+
status: true
3+
dependencies:
4+
config:
5+
- commerce_order.commerce_order_item_type.default
6+
- field.storage.commerce_order_item.field_custom_text
7+
id: commerce_order_item.default.field_custom_text
8+
field_name: field_custom_text
9+
entity_type: commerce_order_item
10+
bundle: default
11+
label: 'Custom text'
12+
description: ''
13+
required: false
14+
translatable: false
15+
default_value: { }
16+
default_value_callback: ''
17+
settings: { }
18+
field_type: string
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
langcode: en
2+
status: true
3+
dependencies:
4+
module:
5+
- commerce_order
6+
id: commerce_order_item.field_custom_text
7+
field_name: field_custom_text
8+
entity_type: commerce_order_item
9+
type: string
10+
settings:
11+
max_length: 255
12+
is_ascii: false
13+
case_sensitive: false
14+
module: core
15+
locked: false
16+
cardinality: 1
17+
translatable: true
18+
indexes: { }
19+
persist_with_no_fields: false
20+
custom_storage: false
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: Extra order item field
2+
type: module
3+
description: Contains field and field storage config for extra order item field
4+
package: Testing
5+
core: 8.x
6+
dependencies:
7+
- commerce:commerce_order

modules/cart/tests/src/Kernel/CartManagerTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class CartManagerTest extends CommerceKernelTestBase {
6565
'state_machine',
6666
'commerce_product',
6767
'commerce_order',
68+
'extra_order_item_field',
6869
];
6970

7071
/**
@@ -76,6 +77,7 @@ protected function setUp() {
7677
$this->installEntitySchema('commerce_order');
7778
$this->installConfig(['commerce_order']);
7879
$this->installConfig(['commerce_product']);
80+
$this->installConfig(['extra_order_item_field']);
7981

8082
$this->variation1 = ProductVariation::create([
8183
'type' => 'default',
@@ -162,4 +164,99 @@ public function testAddOrderItem() {
162164
$this->assertEquals(1, count($cart->getItems()));
163165
}
164166

167+
/**
168+
* Tests that duplicate order items are combined.
169+
*/
170+
public function testAddDuplicateOrderItem() {
171+
$this->installCommerceCart();
172+
173+
$cart = $this->cartProvider->createCart('default', $this->store, $this->user);
174+
$this->assertInstanceOf(OrderInterface::class, $cart);
175+
$this->assertEmpty($cart->getItems());
176+
177+
// First item added.
178+
$order_item1 = $this->cartManager->addEntity($cart, $this->variation1);
179+
$order_item1 = $this->reloadEntity($order_item1);
180+
$this->assertNotEmpty($cart->hasItem($order_item1));
181+
$this->assertEquals(1, $order_item1->getQuantity());
182+
$this->assertEquals($cart->id(), $order_item1->getOrderId());
183+
$this->assertEquals(new Price('1.00', 'USD'), $cart->getTotalPrice());
184+
185+
// Second item should be combined.
186+
$order_item2 = $this->cartManager->addEntity($cart, $this->variation1, 3);
187+
$order_item2 = $this->reloadEntity($order_item2);
188+
$this->assertNotEmpty($cart->hasItem($order_item2));
189+
$this->assertEquals(4, $order_item2->getQuantity());
190+
$this->assertEquals($cart->id(), $order_item2->getOrderId());
191+
$this->assertEquals(new Price('4.00', 'USD'), $cart->getTotalPrice());
192+
193+
// Test FALSE combine flag.
194+
$order_item3 = $this->cartManager->addEntity($cart, $this->variation1, 3, FALSE);
195+
$order_item3 = $this->reloadEntity($order_item3);
196+
$this->assertNotEmpty($cart->hasItem($order_item3));
197+
$this->assertEquals(4, $order_item2->getQuantity());
198+
$this->assertEquals($cart->id(), $order_item2->getOrderId());
199+
$this->assertEquals(3, $order_item3->getQuantity());
200+
$this->assertEquals($cart->id(), $order_item3->getOrderId());
201+
$this->assertEquals(new Price('7.00', 'USD'), $cart->getTotalPrice());
202+
}
203+
204+
/**
205+
* Tests that adding duplicate order items with extra fields results in merging.
206+
*/
207+
public function testAddDuplicateOrderItemExtraField() {
208+
$this->installCommerceCart();
209+
210+
// Add an extra field to the default order item type form display.
211+
$form_display = \Drupal::entityTypeManager()
212+
->getStorage('entity_form_display')
213+
->load('commerce_order_item.default.add_to_cart');
214+
$this->assertNotEmpty($form_display);
215+
$form_display->setComponent('field_custom_text', [
216+
'type' => 'string_textfield',
217+
]);
218+
$form_display->save();
219+
220+
$cart = $this->cartProvider->createCart('default', $this->store, $this->user);
221+
$this->assertInstanceOf(OrderInterface::class, $cart);
222+
$this->assertEmpty($cart->getItems());
223+
224+
// Add order item with custom text.
225+
$order_item1 = $this->cartManager->createOrderItem($this->variation1);
226+
$order_item1->set('field_custom_text', 'Blue');
227+
$order_item1->save();
228+
$order_item1 = $this->cartManager->addOrderItem($cart, $order_item1);
229+
$order_item1 = $this->reloadEntity($order_item1);
230+
$this->assertNotEmpty($cart->hasItem($order_item1));
231+
$this->assertEquals(1, $order_item1->getQuantity());
232+
$this->assertEquals($cart->id(), $order_item1->getOrderId());
233+
$this->assertEquals(new Price('1.00', 'USD'), $cart->getTotalPrice());
234+
235+
// Second item for same variation, different text should not be combined.
236+
$order_item2 = $this->cartManager->createOrderItem($this->variation1, 3);
237+
$order_item2->set('field_custom_text', 'Red');
238+
$order_item2->save();
239+
$order_item2 = $this->cartManager->addOrderItem($cart, $order_item2);
240+
$order_item2 = $this->reloadEntity($order_item2);
241+
$this->assertEquals(1, $order_item1->getQuantity());
242+
$this->assertEquals($cart->id(), $order_item1->getOrderId());
243+
$this->assertNotEmpty($cart->hasItem($order_item2));
244+
$this->assertEquals(3, $order_item2->getQuantity());
245+
$this->assertEquals($cart->id(), $order_item2->getOrderId());
246+
$this->assertEquals(new Price('4.00', 'USD'), $cart->getTotalPrice());
247+
248+
// Third item should be combined with first.
249+
$order_item3 = $this->cartManager->createOrderItem($this->variation1, 3);
250+
$order_item3->set('field_custom_text', 'Blue');
251+
$order_item3->save();
252+
$order_item3 = $this->cartManager->addOrderItem($cart, $order_item3);
253+
$order_item3 = $this->reloadEntity($order_item3);
254+
$this->assertNotEmpty($cart->hasItem($order_item2));
255+
$this->assertEquals(3, $order_item2->getQuantity());
256+
$this->assertNotEmpty($cart->hasItem($order_item3));
257+
$this->assertEquals(4, $order_item3->getQuantity());
258+
$this->assertEquals($cart->id(), $order_item3->getOrderId());
259+
$this->assertEquals(new Price('7.00', 'USD'), $cart->getTotalPrice());
260+
}
261+
165262
}

0 commit comments

Comments
 (0)