Skip to content

Commit 7e90fb1

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-4061' into PR_2025_08_04_chittima
2 parents 15ce386 + d572cfc commit 7e90fb1

File tree

4 files changed

+272
-4
lines changed

4 files changed

+272
-4
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
* Convert product options from serialized string to array format.
30+
*
31+
* @param string $productOptions
32+
* @return array
33+
*/
34+
private function getProductOptionsArray(string $productOptions): array
35+
{
36+
try {
37+
$options = $this->serializer->unserialize($productOptions);
38+
} catch (\Exception $e) {
39+
$options = [];
40+
}
41+
return $options;
42+
}
43+
44+
/**
45+
* Retrieve existing order item row by item ID.
46+
*
47+
* @param int $itemId
48+
* @return array
49+
*/
50+
private function getExistingOrderItemProductOptions(int $itemId): array
51+
{
52+
$productOptions = [];
53+
try {
54+
$row = $this->orderItemResource->getConnection()
55+
->fetchRow(
56+
$this->orderItemResource->getConnection()->select()
57+
->from($this->orderItemResource->getMainTable())
58+
->where('item_id = ?', $itemId)
59+
);
60+
if (isset($row['product_options']) && is_string($row['product_options'])) {
61+
$productOptions = $this->getProductOptionsArray($row['product_options']);
62+
}
63+
} catch (\Exception $e) {
64+
$productOptions = [];
65+
}
66+
return $productOptions;
67+
}
68+
69+
/**
70+
* Add existing item product options to the order items before processing the relation.
71+
*
72+
* @param Relation $subject
73+
* @param AbstractModel $object
74+
* @return void
75+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
76+
*/
77+
public function beforeProcessRelation(Relation $subject, AbstractModel $object): void
78+
{
79+
if ($object instanceof OrderInterface && $object->getId() && $object->getItems()) {
80+
foreach ($object->getItems() as $item) {
81+
if ($item->getItemId()) {
82+
$productOptions = $this->getExistingOrderItemProductOptions((int)$item->getItemId());
83+
if (count($productOptions)) {
84+
$item->setProductOptions($productOptions);
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}

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)