Skip to content

Commit 51188f4

Browse files
committed
Merge remote-tracking branch 'origin/AC-9244' into spartans_pr_10122025
2 parents ba4bf64 + 0a7242a commit 51188f4

File tree

4 files changed

+366
-0
lines changed

4 files changed

+366
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Plugin\Webapi;
9+
10+
use Magento\Framework\Webapi\ServiceOutputProcessor;
11+
use Magento\Sales\Api\OrderRepositoryInterface;
12+
13+
/**
14+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
15+
*/
16+
class OrderResponseNullKeysPlugin
17+
{
18+
/**
19+
* Ensure state/status keys exist as null for order responses, so REST includes them.
20+
*
21+
* @param ServiceOutputProcessor $subject
22+
* @param mixed $result
23+
* @param mixed $data
24+
* @param string $serviceClassName
25+
* @param string $serviceMethodName
26+
* @return array|mixed
27+
*
28+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
29+
*/
30+
public function afterProcess(
31+
ServiceOutputProcessor $subject,
32+
$result,
33+
$data,
34+
string $serviceClassName,
35+
string $serviceMethodName
36+
) {
37+
if ($serviceClassName !== OrderRepositoryInterface::class) {
38+
return $result;
39+
}
40+
41+
if ($serviceMethodName === 'get' && is_array($result)) {
42+
return $this->ensureOrderKeys($result);
43+
}
44+
45+
if ($serviceMethodName === 'getList' &&
46+
is_array($result) &&
47+
isset($result['items'])
48+
&& is_array($result['items'])
49+
) {
50+
foreach ($result['items'] as $i => $item) {
51+
if (is_array($item)) {
52+
$result['items'][$i] = $this->ensureOrderKeys($item);
53+
}
54+
}
55+
}
56+
57+
return $result;
58+
}
59+
60+
/**
61+
* If state and status key missing then set as null
62+
*
63+
* @param array $order
64+
* @return array
65+
*/
66+
private function ensureOrderKeys(array $order): array
67+
{
68+
if (!array_key_exists('state', $order)) {
69+
$order['state'] = null;
70+
}
71+
if (!array_key_exists('status', $order)) {
72+
$order['status'] = null;
73+
}
74+
return $order;
75+
}
76+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Test\Unit\Plugin\Webapi;
9+
10+
use Magento\Framework\Webapi\ServiceOutputProcessor;
11+
use Magento\Sales\Api\OrderRepositoryInterface;
12+
use Magento\Sales\Plugin\Webapi\OrderResponseNullKeysPlugin;
13+
use PHPUnit\Framework\MockObject\MockObject;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class OrderResponseNullKeysPluginTest extends TestCase
17+
{
18+
/** @var ServiceOutputProcessor|MockObject */
19+
private $serviceOutputProcessor;
20+
21+
/** @var OrderResponseNullKeysPlugin */
22+
private $plugin;
23+
24+
protected function setUp(): void
25+
{
26+
$this->serviceOutputProcessor = $this->createMock(ServiceOutputProcessor::class);
27+
$this->plugin = new OrderResponseNullKeysPlugin();
28+
}
29+
30+
public function testAfterProcessNonOrderRepositoryReturnsUnchanged(): void
31+
{
32+
$input = ['foo' => 'bar'];
33+
34+
$result = $this->plugin->afterProcess(
35+
$this->serviceOutputProcessor,
36+
$input,
37+
[],
38+
\Magento\Catalog\Api\ProductRepositoryInterface::class,
39+
'get'
40+
);
41+
42+
$this->assertSame($input, $result);
43+
}
44+
45+
public function testAfterProcessGetAddsNullKeysWhenMissing(): void
46+
{
47+
$input = ['entity_id' => 10];
48+
49+
$result = $this->plugin->afterProcess(
50+
$this->serviceOutputProcessor,
51+
$input,
52+
[],
53+
OrderRepositoryInterface::class,
54+
'get'
55+
);
56+
57+
$this->assertArrayHasKey('entity_id', $result);
58+
$this->assertSame(10, $result['entity_id']);
59+
$this->assertArrayHasKey('state', $result);
60+
$this->assertNull($result['state']);
61+
$this->assertArrayHasKey('status', $result);
62+
$this->assertNull($result['status']);
63+
}
64+
65+
public function testAfterProcessGetDoesNotOverwriteExistingKeys(): void
66+
{
67+
$input = ['entity_id' => 10, 'state' => 'processing', 'status' => 'processing'];
68+
69+
$result = $this->plugin->afterProcess(
70+
$this->serviceOutputProcessor,
71+
$input,
72+
[],
73+
OrderRepositoryInterface::class,
74+
'get'
75+
);
76+
77+
$this->assertSame('processing', $result['state']);
78+
$this->assertSame('processing', $result['status']);
79+
}
80+
81+
public function testAfterProcessGetListAddsNullKeysPerItem(): void
82+
{
83+
$input = [
84+
'items' => [
85+
['entity_id' => 1, 'state' => 'processing', 'status' => 'processing'],
86+
['entity_id' => 2],
87+
['entity_id' => 3, 'state' => null], // state exists (null); status missing
88+
],
89+
'search_criteria' => [],
90+
'total_count' => 3,
91+
];
92+
93+
$result = $this->plugin->afterProcess(
94+
$this->serviceOutputProcessor,
95+
$input,
96+
[],
97+
OrderRepositoryInterface::class,
98+
'getList'
99+
);
100+
101+
$this->assertArrayHasKey('items', $result);
102+
$this->assertCount(3, $result['items']);
103+
104+
// Item 0 unchanged
105+
$this->assertSame('processing', $result['items'][0]['state']);
106+
$this->assertSame('processing', $result['items'][0]['status']);
107+
108+
// Item 1 added nulls
109+
$this->assertArrayHasKey('state', $result['items'][1]);
110+
$this->assertNull($result['items'][1]['state']);
111+
$this->assertArrayHasKey('status', $result['items'][1]);
112+
$this->assertNull($result['items'][1]['status']);
113+
114+
// Item 2 keeps existing state (null) and adds missing status
115+
$this->assertArrayHasKey('state', $result['items'][2]);
116+
$this->assertNull($result['items'][2]['state']);
117+
$this->assertArrayHasKey('status', $result['items'][2]);
118+
$this->assertNull($result['items'][2]['status']);
119+
}
120+
121+
public function testAfterProcessGetListWithoutItemsRemainsUnchanged(): void
122+
{
123+
$input = ['search_criteria' => [], 'total_count' => 0];
124+
125+
$result = $this->plugin->afterProcess(
126+
$this->serviceOutputProcessor,
127+
$input,
128+
[],
129+
OrderRepositoryInterface::class,
130+
'getList'
131+
);
132+
133+
$this->assertSame($input, $result);
134+
}
135+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@
3131
type="Magento\Sales\Plugin\Model\OrderRepositoryPlugin"
3232
sortOrder="10"/>
3333
</type>
34+
<type name="Magento\Framework\Webapi\ServiceOutputProcessor">
35+
<plugin name="sales_rest_add_null_state_status"
36+
type="Magento\Sales\Plugin\Webapi\OrderResponseNullKeysPlugin"
37+
sortOrder="10"/>
38+
</type>
3439
</config>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Sales\Service\V1;
10+
11+
use Magento\Authorization\Test\Fixture\Role;
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\Framework\Exception\AuthenticationException;
20+
use Magento\Framework\Exception\InputException;
21+
use Magento\Framework\Exception\LocalizedException;
22+
use Magento\Integration\Api\AdminTokenServiceInterface;
23+
use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture;
24+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
25+
use Magento\TestFramework\Fixture\Config as ConfigFixture;
26+
use Magento\TestFramework\Fixture\DataFixture;
27+
use Magento\TestFramework\Fixture\DataFixtureStorage;
28+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
29+
use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper;
30+
use Magento\TestFramework\TestCase\WebapiAbstract;
31+
use Magento\User\Test\Fixture\User;
32+
use Magento\TestFramework\Helper\Bootstrap;
33+
use Magento\Framework\App\ResourceConnection;
34+
use Magento\Framework\DB\Sql\Expression;
35+
36+
class OrderResponseNullKeysTest extends WebapiAbstract
37+
{
38+
/**
39+
* @var DataFixtureStorage
40+
*/
41+
private $fixtures;
42+
43+
/**
44+
* @var AdminTokenServiceInterface
45+
*/
46+
private $adminToken;
47+
48+
/**
49+
* @inheritdoc
50+
*/
51+
protected function setUp(): void
52+
{
53+
parent::setUp();
54+
$this->_markTestAsRestOnly();
55+
$this->fixtures = BootstrapHelper::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage();
56+
$this->adminToken = BootstrapHelper::getObjectManager()->get(AdminTokenServiceInterface::class);
57+
}
58+
59+
#[
60+
DataFixture(Role::class, as: 'allRole'),
61+
DataFixture(User::class, ['role_id' => '$allRole.id$'], as: 'allUser'),
62+
ConfigFixture('cataloginventory/item_options/auto_return', 0),
63+
ConfigFixture('payment/checkmo/active', '1'),
64+
ConfigFixture('carriers/flatrate/active', '1'),
65+
DataFixture(ProductFixture::class, [
66+
'price' => 10.00,
67+
'quantity_and_stock_status' => ['qty' => 100, 'is_in_stock' => true]
68+
], as: 'product'),
69+
DataFixture(GuestCartFixture::class, as: 'cart'),
70+
DataFixture(SetGuestEmailFixture::class, [
71+
'cart_id' => '$cart.id$',
72+
'email' => '[email protected]'
73+
]),
74+
DataFixture(AddProductToCartFixture::class, [
75+
'cart_id' => '$cart.id$',
76+
'product_id' => '$product.id$',
77+
'qty' => 1
78+
]),
79+
DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
80+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
81+
DataFixture(SetDeliveryMethodFixture::class, [
82+
'cart_id' => '$cart.id$',
83+
'carrier_code' => 'flatrate',
84+
'method_code' => 'flatrate'
85+
]),
86+
DataFixture(SetPaymentMethodFixture::class, [
87+
'cart_id' => '$cart.id$',
88+
'method' => 'checkmo'
89+
]),
90+
DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], as: 'order'),
91+
]
92+
public function testUserWithRestrictedWebsiteAndStoreGroup()
93+
{
94+
$order = $this->fixtures->get('order');
95+
$orderId = (int) $order->getId();
96+
$this->nullifyOrderStateStatus($orderId);
97+
98+
$user = $this->fixtures->get('allUser');
99+
$accessToken = $this->getAccessToken($user->getUsername());
100+
$serviceInfo = [
101+
'rest' => [
102+
'resourcePath' => '/V1/orders/' . $orderId,
103+
'httpMethod' => 'GET',
104+
'token' => $accessToken
105+
]
106+
];
107+
$result = $this->_webApiCall($serviceInfo);
108+
109+
$this->assertIsArray($result);
110+
$this->assertSame($orderId, (int)$result['entity_id']);
111+
$this->assertArrayHasKey('state', $result);
112+
$this->assertArrayHasKey('status', $result);
113+
$this->assertNull($result['state']);
114+
$this->assertNull($result['status']);
115+
}
116+
117+
/**
118+
* Update order status and state field as null
119+
*
120+
* @param int $orderId
121+
* @return void
122+
*/
123+
private function nullifyOrderStateStatus(int $orderId): void
124+
{
125+
$om = Bootstrap::getObjectManager();
126+
$resource = $om->get(ResourceConnection::class);
127+
$connection = $resource->getConnection();
128+
$table = $resource->getTableName('sales_order');
129+
$connection->update(
130+
$table,
131+
['state' => new Expression('NULL'), 'status' => new Expression('NULL')],
132+
['entity_id = ?' => $orderId]
133+
);
134+
}
135+
136+
/**
137+
* Get admin access token
138+
*
139+
* @param string $username
140+
* @param string $password
141+
* @return string
142+
* @throws AuthenticationException
143+
* @throws InputException
144+
* @throws LocalizedException
145+
*/
146+
private function getAccessToken(string $username, string $password = 'password1'): string
147+
{
148+
return $this->adminToken->createAdminAccessToken($username, $password);
149+
}
150+
}

0 commit comments

Comments
 (0)