Skip to content

Commit 7c98c99

Browse files
committed
Merge branch 'ACP2E-153' of https://github.com/magento-l3/magento2ce into PR-2021-20-08
2 parents a7d1290 + fb66474 commit 7c98c99

File tree

5 files changed

+171
-16
lines changed

5 files changed

+171
-16
lines changed

app/code/Magento/Sales/Model/InvoiceOrder.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Magento\Sales\Model;
88

9+
use Magento\Framework\App\ObjectManager;
910
use Magento\Framework\App\ResourceConnection;
1011
use Magento\Sales\Api\Data\InvoiceCommentCreationInterface;
1112
use Magento\Sales\Api\Data\InvoiceCreationArgumentsInterface;
@@ -21,7 +22,8 @@
2122
use Psr\Log\LoggerInterface;
2223

2324
/**
24-
* Class InvoiceOrder
25+
* Creates invoice for an order
26+
*
2527
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2628
*/
2729
class InvoiceOrder implements InvoiceOrderInterface
@@ -76,6 +78,11 @@ class InvoiceOrder implements InvoiceOrderInterface
7678
*/
7779
private $logger;
7880

81+
/**
82+
* @var OrderMutexInterface
83+
*/
84+
private $orderMutex;
85+
7986
/**
8087
* InvoiceOrder constructor.
8188
* @param ResourceConnection $resourceConnection
@@ -88,6 +95,7 @@ class InvoiceOrder implements InvoiceOrderInterface
8895
* @param InvoiceOrderValidator $invoiceOrderValidator
8996
* @param NotifierInterface $notifierInterface
9097
* @param LoggerInterface $logger
98+
* @param OrderMutex|null $orderMutex
9199
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
92100
*/
93101
public function __construct(
@@ -100,7 +108,8 @@ public function __construct(
100108
InvoiceRepository $invoiceRepository,
101109
InvoiceOrderValidator $invoiceOrderValidator,
102110
NotifierInterface $notifierInterface,
103-
LoggerInterface $logger
111+
LoggerInterface $logger,
112+
?OrderMutexInterface $orderMutex = null
104113
) {
105114
$this->resourceConnection = $resourceConnection;
106115
$this->orderRepository = $orderRepository;
@@ -112,9 +121,12 @@ public function __construct(
112121
$this->invoiceOrderValidator = $invoiceOrderValidator;
113122
$this->notifierInterface = $notifierInterface;
114123
$this->logger = $logger;
124+
$this->orderMutex = $orderMutex ?: ObjectManager::getInstance()->get(OrderMutexInterface::class);
115125
}
116126

117127
/**
128+
* Creates invoice for provided order ID
129+
*
118130
* @param int $orderId
119131
* @param bool $capture
120132
* @param array $items
@@ -138,7 +150,48 @@ public function execute(
138150
InvoiceCommentCreationInterface $comment = null,
139151
InvoiceCreationArgumentsInterface $arguments = null
140152
) {
141-
$connection = $this->resourceConnection->getConnection('sales');
153+
return $this->orderMutex->execute(
154+
(int) $orderId,
155+
\Closure::fromCallable([$this, 'createInvoice']),
156+
[
157+
$orderId,
158+
$capture,
159+
$items,
160+
$notify,
161+
$appendComment,
162+
$comment,
163+
$arguments
164+
]
165+
);
166+
}
167+
168+
/**
169+
* Creates invoice for provided order ID
170+
*
171+
* @param int $orderId
172+
* @param bool $capture
173+
* @param array $items
174+
* @param bool $notify
175+
* @param bool $appendComment
176+
* @param \Magento\Sales\Api\Data\InvoiceCommentCreationInterface|null $comment
177+
* @param \Magento\Sales\Api\Data\InvoiceCreationArgumentsInterface|null $arguments
178+
* @return int
179+
* @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface
180+
* @throws \Magento\Sales\Api\Exception\CouldNotInvoiceExceptionInterface
181+
* @throws \Magento\Framework\Exception\InputException
182+
* @throws \Magento\Framework\Exception\NoSuchEntityException
183+
* @throws \DomainException
184+
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
185+
*/
186+
private function createInvoice(
187+
$orderId,
188+
$capture = false,
189+
array $items = [],
190+
$notify = false,
191+
$appendComment = false,
192+
InvoiceCommentCreationInterface $comment = null,
193+
InvoiceCreationArgumentsInterface $arguments = null
194+
) {
142195
$order = $this->orderRepository->get($orderId);
143196
$invoice = $this->invoiceDocumentFactory->create(
144197
$order,
@@ -162,7 +215,6 @@ public function execute(
162215
__("Invoice Document Validation Error(s):\n" . implode("\n", $errorMessages->getMessages()))
163216
);
164217
}
165-
$connection->beginTransaction();
166218
try {
167219
$order = $this->paymentAdapter->pay($order, $invoice, $capture);
168220
$order->setState(
@@ -172,10 +224,8 @@ public function execute(
172224
$invoice->setState(\Magento\Sales\Model\Order\Invoice::STATE_PAID);
173225
$this->invoiceRepository->save($invoice);
174226
$this->orderRepository->save($order);
175-
$connection->commit();
176227
} catch (\Exception $e) {
177228
$this->logger->critical($e);
178-
$connection->rollBack();
179229
throw new \Magento\Sales\Exception\CouldNotInvoiceException(
180230
__('Could not save an invoice, see error log for details')
181231
);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Model;
9+
10+
use Magento\Framework\App\ResourceConnection;
11+
12+
/**
13+
* Intended to prevent race conditions during order update by concurrent requests.
14+
*/
15+
class OrderMutex implements OrderMutexInterface
16+
{
17+
/**
18+
* @var ResourceConnection
19+
*/
20+
private $resourceConnection;
21+
22+
/**
23+
* @param ResourceConnection $resourceConnection
24+
*/
25+
public function __construct(
26+
ResourceConnection $resourceConnection
27+
) {
28+
$this->resourceConnection = $resourceConnection;
29+
}
30+
31+
/**
32+
* @inheritdoc
33+
*/
34+
public function execute(int $orderId, callable $callable, array $args = [])
35+
{
36+
$connection = $this->resourceConnection->getConnection('sales');
37+
$connection->beginTransaction();
38+
$query = $connection->select()
39+
->from($this->resourceConnection->getTableName('sales_order'), 'entity_id')
40+
->where('entity_id = ?', $orderId)
41+
->forUpdate(true);
42+
$connection->query($query);
43+
44+
try {
45+
$result = $callable(...$args);
46+
$connection->commit();
47+
return $result;
48+
} catch (\Throwable $e) {
49+
$connection->rollBack();
50+
throw $e;
51+
}
52+
}
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Model;
9+
10+
/**
11+
* Intended to prevent race conditions during order update by concurrent requests.
12+
*/
13+
interface OrderMutexInterface
14+
{
15+
/**
16+
* Acquires a lock for order, executes callable and releases the lock after.
17+
*
18+
* @param int $orderId
19+
* @param callable $callable
20+
* @param array $args
21+
* @return mixed
22+
*/
23+
public function execute(int $orderId, callable $callable, array $args = []);
24+
}

app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Magento\Framework\App\ResourceConnection;
1111
use Magento\Framework\DB\Adapter\AdapterInterface;
12+
use Magento\Framework\DB\Select;
1213
use Magento\Sales\Api\Data\InvoiceCommentCreationInterface;
1314
use Magento\Sales\Api\Data\InvoiceCreationArgumentsInterface;
1415
use Magento\Sales\Api\Data\InvoiceInterface;
@@ -26,6 +27,7 @@
2627
use Magento\Sales\Model\Order\OrderStateResolverInterface;
2728
use Magento\Sales\Model\Order\PaymentAdapterInterface;
2829
use Magento\Sales\Model\Order\Validation\InvoiceOrderInterface;
30+
use Magento\Sales\Model\OrderMutex;
2931
use Magento\Sales\Model\ValidatorResultInterface;
3032
use PHPUnit\Framework\MockObject\MockObject;
3133
use PHPUnit\Framework\TestCase;
@@ -200,7 +202,8 @@ protected function setUp(): void
200202
$this->invoiceRepositoryMock,
201203
$this->invoiceOrderValidatorMock,
202204
$this->notifierInterfaceMock,
203-
$this->loggerMock
205+
$this->loggerMock,
206+
new OrderMutex($this->resourceConnectionMock)
204207
);
205208
}
206209

@@ -216,10 +219,7 @@ protected function setUp(): void
216219
*/
217220
public function testOrderInvoice($orderId, $capture, $items, $notify, $appendComment)
218221
{
219-
$this->resourceConnectionMock->expects($this->once())
220-
->method('getConnection')
221-
->with('sales')
222-
->willReturn($this->adapterInterface);
222+
$this->mockConnection($orderId);
223223
$this->orderRepositoryMock->expects($this->once())
224224
->method('get')
225225
->willReturn($this->orderMock);
@@ -315,7 +315,7 @@ public function testDocumentValidationException()
315315
$notify = true;
316316
$appendComment = true;
317317
$errorMessages = ['error1', 'error2'];
318-
318+
$this->mockConnection($orderId);
319319
$this->orderRepositoryMock->expects($this->once())
320320
->method('get')
321321
->willReturn($this->orderMock);
@@ -369,10 +369,7 @@ public function testCouldNotInvoiceException()
369369
$capture = true;
370370
$notify = true;
371371
$appendComment = true;
372-
$this->resourceConnectionMock->expects($this->once())
373-
->method('getConnection')
374-
->with('sales')
375-
->willReturn($this->adapterInterface);
372+
$this->mockConnection($orderId);
376373

377374
$this->orderRepositoryMock->expects($this->once())
378375
->method('get')
@@ -440,4 +437,34 @@ public function dataProvider()
440437
'TestWithNotifyFalse' => [1, true, [1 => 2], false, true],
441438
];
442439
}
440+
441+
/**
442+
* @param int $orderId
443+
*/
444+
private function mockConnection(int $orderId): void
445+
{
446+
$select = $this->createMock(Select::class);
447+
$select->expects($this->once())
448+
->method('from')
449+
->with('sales_order', 'entity_id')
450+
->willReturnSelf();
451+
$select->expects($this->once())
452+
->method('where')
453+
->with('entity_id = ?', $orderId)
454+
->willReturnSelf();
455+
$select->expects($this->once())
456+
->method('forUpdate')
457+
->with(true)
458+
->willReturnSelf();
459+
$this->adapterInterface->expects($this->once())
460+
->method('select')
461+
->willReturn($select);
462+
$this->resourceConnectionMock->expects($this->once())
463+
->method('getConnection')
464+
->with('sales')
465+
->willReturn($this->adapterInterface);
466+
$this->resourceConnectionMock->expects($this->once())
467+
->method('getTableName')
468+
->willReturnArgument(0);
469+
}
443470
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
<preference for="Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProviderInterface" type="Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProvider" />
119119
<preference for="Magento\Sales\Model\ConfigInterface" type="Magento\Sales\Model\Config" />
120120
<preference for="Magento\Sales\Model\Order\Shipment\ShipmentItemsValidatorInterface" type="Magento\Sales\Model\Order\Shipment\ShipmentItemsValidator" />
121+
<preference for="Magento\Sales\Model\OrderMutexInterface" type="Magento\Sales\Model\OrderMutex"/>
121122
<type name="Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProvider">
122123
<arguments>
123124
<argument name="providers" xsi:type="array">

0 commit comments

Comments
 (0)