Skip to content

Commit 69cf997

Browse files
committed
ACP2E-4061: Issue with Updated Orders with Configurable Options Using REST API
1 parent fab20b0 commit 69cf997

File tree

4 files changed

+273
-4
lines changed

4 files changed

+273
-4
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Plugin\Model\ResourceModel\Order\Relation;
9+
10+
use Magento\Framework\Model\AbstractModel;
11+
use Magento\Framework\Serialize\Serializer\Json;
12+
use Magento\Sales\Api\Data\OrderInterface;
13+
use Magento\Sales\Model\ResourceModel\Order\Item as OrderItemResource;
14+
use Magento\Sales\Model\ResourceModel\Order\Relation;
15+
16+
class AddExistingItemProductOptions
17+
{
18+
/**
19+
* @param OrderItemResource $orderItemResource
20+
* @param Json $serializer
21+
*/
22+
public function __construct(
23+
private readonly OrderItemResource $orderItemResource,
24+
private readonly Json $serializer
25+
) {
26+
}
27+
28+
29+
/**
30+
* Convert product options from serialized string to array format.
31+
*
32+
* @param string $productOptions
33+
* @return array
34+
*/
35+
private function getProductOptionsArray(string $productOptions): array
36+
{
37+
try {
38+
$options = $this->serializer->unserialize($productOptions);
39+
} catch (\Exception $e) {
40+
$options = [];
41+
}
42+
return $options;
43+
}
44+
45+
/**
46+
* Retrieve existing order item row by item ID.
47+
*
48+
* @param int $itemId
49+
* @return array
50+
*/
51+
private function getExistingOrderItemProductOptions(int $itemId): array
52+
{
53+
$productOptions = [];
54+
try {
55+
$row = $this->orderItemResource->getConnection()
56+
->fetchRow(
57+
$this->orderItemResource->getConnection()->select()
58+
->from($this->orderItemResource->getMainTable())
59+
->where('item_id = ?', $itemId)
60+
);
61+
if (isset($row['product_options']) && is_string($row['product_options'])) {
62+
$productOptions = $this->getProductOptionsArray($row['product_options']);
63+
}
64+
} catch (\Exception $e) {
65+
$productOptions = [];
66+
}
67+
return $productOptions;
68+
}
69+
70+
/**
71+
* Add existing item product options to the order items before processing the relation.
72+
*
73+
* @param Relation $subject
74+
* @param AbstractModel $object
75+
* @return void
76+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
77+
*/
78+
public function beforeProcessRelation(Relation $subject, AbstractModel $object): void
79+
{
80+
if ($object instanceof OrderInterface && $object->getId() && $object->getItems()) {
81+
foreach ($object->getItems() as $item) {
82+
if ($item->getItemId()) {
83+
$productOptions = $this->getExistingOrderItemProductOptions($item->getItemId());
84+
if (count($productOptions)) {
85+
$item->setProductOptions($productOptions);
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}

app/code/Magento/Sales/etc/webapi_rest/di.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
4+
* Copyright 2011 Adobe
5+
* All Rights Reserved.
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
@@ -22,4 +22,8 @@
2222
<type name="Magento\Sales\Model\Service\InvoiceService">
2323
<plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/>
2424
</type>
25+
<type name="Magento\Sales\Model\ResourceModel\Order\Relation">
26+
<plugin name="add_existing_product_options"
27+
type="Magento\Sales\Plugin\Model\ResourceModel\Order\Relation\AddExistingItemProductOptions"/>
28+
</type>
2529
</config>

app/code/Magento/Sales/etc/webapi_soap/di.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
4+
* Copyright 2011 Adobe
5+
* All Rights Reserved.
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
@@ -22,4 +22,8 @@
2222
<type name="Magento\Sales\Model\Service\InvoiceService">
2323
<plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/>
2424
</type>
25+
<type name="Magento\Sales\Model\ResourceModel\Order\Relation">
26+
<plugin name="add_existing_product_options"
27+
type="Magento\Sales\Plugin\Model\ResourceModel\Order\Relation\AddExistingItemProductOptions"/>
28+
</type>
2529
</config>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Service\V1;
9+
10+
use Magento\Catalog\Test\Fixture\Attribute;
11+
use Magento\Catalog\Test\Fixture\Category as CategoryFixture;
12+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
13+
use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture;
14+
use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture;
15+
use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture;
16+
use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture;
17+
use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture;
18+
use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture;
19+
use Magento\ConfigurableProduct\Test\Fixture\AddProductToCart as AddConfigurableProductToCartFixture;
20+
use Magento\ConfigurableProduct\Test\Fixture\Product as ConfigurableProductFixture;
21+
use Magento\Framework\DataObject;
22+
use Magento\Framework\Webapi\Rest\Request;
23+
use Magento\Indexer\Test\Fixture\Indexer;
24+
use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture;
25+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
26+
use Magento\Sales\Api\OrderItemRepositoryInterface;
27+
use Magento\TestFramework\Fixture\AppArea;
28+
use Magento\TestFramework\Fixture\DataFixture;
29+
use Magento\TestFramework\Fixture\DataFixtureStorage;
30+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
31+
use Magento\TestFramework\ObjectManager;
32+
use Magento\TestFramework\TestCase\WebapiAbstract;
33+
34+
class OrderUpdateV1Test extends WebapiAbstract
35+
{
36+
private const RESOURCE_PATH = '/V1/orders';
37+
38+
private const SERVICE_NAME = 'salesOrderRepositoryV1';
39+
40+
private const SERVICE_VERSION = 'V1';
41+
42+
/**
43+
* @var DataFixtureStorage
44+
*/
45+
private DataFixtureStorage $fixture;
46+
47+
/**
48+
* @var OrderItemRepositoryInterface
49+
*/
50+
private OrderItemRepositoryInterface $orderItemRepository;
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
protected function setUp(): void
56+
{
57+
$this->fixture = DataFixtureStorageManager::getStorage();
58+
$this->orderItemRepository = ObjectManager::getInstance()->get(OrderItemRepositoryInterface::class);
59+
}
60+
61+
#[
62+
AppArea('adminhtml'),
63+
DataFixture(
64+
Attribute::class,
65+
[
66+
'frontend_input' => 'select',
67+
'backend_type' => 'int',
68+
'options' => [
69+
['label' => 'option1', 'sort_order' => 0],
70+
['label' => 'option2', 'sort_order' => 1]
71+
]
72+
],
73+
as: 'attr'
74+
),
75+
DataFixture(CategoryFixture::class, ['name' => 'Category'], 'category'),
76+
DataFixture(ProductFixture::class, ['price' => 200, 'category_ids' => ['$category.id$']], as: 'simple1'),
77+
DataFixture(ProductFixture::class, ['price' => 100, 'category_ids' => ['$category.id$']], as: 'simple2'),
78+
DataFixture(
79+
ConfigurableProductFixture::class,
80+
[
81+
'_options' => ['$attr$'], '_links' => ['$simple2$']
82+
],
83+
as: 'cp1'
84+
),
85+
DataFixture(Indexer::class, as: 'indexer'),
86+
DataFixture(GuestCartFixture::class, as: 'cart'),
87+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$simple1.id$']),
88+
DataFixture(
89+
AddConfigurableProductToCartFixture::class,
90+
['cart_id' => '$cart.id$', 'product_id' => '$cp1.id$', 'child_product_id' => '$simple2.id$', 'qty' => 1]
91+
),
92+
DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
93+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
94+
DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']),
95+
DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']),
96+
DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']),
97+
DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order')
98+
]
99+
public function testOrderUpdate()
100+
{
101+
$order = $this->fixture->get('order');
102+
$productOptions = [];
103+
foreach ($order->getItems() as $item) {
104+
if ($item->getProductType() === 'simple') {
105+
$productOptions[] = $item->getProductOptions();
106+
}
107+
}
108+
109+
$getResult = $this->makeGetServiceCall($order);
110+
$this->makePostServiceCall($getResult);
111+
112+
$resavedProductOptions = [];
113+
foreach ($order->getItems() as $item) {
114+
if ($item->getProductType() === 'simple') {
115+
$item = $this->orderItemRepository->get($item->getItemId());
116+
$resavedProductOptions[] = $item->getProductOptions();
117+
}
118+
}
119+
120+
$this->assertEquals(
121+
json_encode($productOptions),
122+
json_encode($resavedProductOptions),
123+
'Product Options do not match.'
124+
);
125+
}
126+
127+
/**
128+
* Makes GET service call.
129+
*
130+
* @param DataObject $order
131+
* @return array
132+
*/
133+
private function makeGetServiceCall(DataObject $order): array
134+
{
135+
$serviceInfo = [
136+
'rest' => [
137+
'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(),
138+
'httpMethod' => Request::HTTP_METHOD_GET,
139+
],
140+
'soap' => [
141+
'service' => self::SERVICE_NAME,
142+
'serviceVersion' => self::SERVICE_VERSION,
143+
'operation' => self::SERVICE_NAME . 'get',
144+
],
145+
];
146+
return $this->_webApiCall($serviceInfo, ['id' => $order->getId()]);
147+
}
148+
149+
/**
150+
* Makes POST service call.
151+
*
152+
* @param $orderData
153+
* @return array
154+
*/
155+
private function makePostServiceCall($orderData): array
156+
{
157+
$serviceInfo = [
158+
'rest' => [
159+
'resourcePath' => self::RESOURCE_PATH,
160+
'httpMethod' => Request::HTTP_METHOD_POST,
161+
],
162+
'soap' => [
163+
'service' => self::SERVICE_NAME,
164+
'serviceVersion' => self::SERVICE_VERSION,
165+
'operation' => self::SERVICE_NAME . 'save',
166+
],
167+
];
168+
return $this->_webApiCall($serviceInfo, ['entity' => $orderData]);
169+
}
170+
}

0 commit comments

Comments
 (0)