Skip to content

Commit 1a60da0

Browse files
Merge branch 'AC-12802' into cia-2.4.8-beta2-develop-bugfix-10272024
2 parents 272e906 + e5b5ba0 commit 1a60da0

File tree

4 files changed

+111
-11
lines changed

4 files changed

+111
-11
lines changed

app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public function execute(CartInterface $quote, bool $increment): void
7070
$updateInfo->setCustomerId((int)$quote->getCustomerId());
7171
$updateInfo->setIsIncrement($increment);
7272

73-
$this->couponUsagePublisher->publish($updateInfo);
74-
$this->processor->updateCustomerRulesUsages($updateInfo);
7573
$this->processor->updateCouponUsages($updateInfo);
74+
$this->processor->updateCustomerRulesUsages($updateInfo);
75+
$this->couponUsagePublisher->publish($updateInfo);
7676
}
7777
}

app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,62 @@
77

88
namespace Magento\SalesRule\Model\Coupon\Usage;
99

10+
use Exception;
1011
use Magento\Framework\Api\SearchCriteriaBuilder;
12+
use Magento\Framework\App\ObjectManager;
13+
use Magento\Framework\Exception\CouldNotSaveException;
14+
use Magento\Framework\Exception\LocalizedException;
15+
use Magento\Framework\Exception\NoSuchEntityException;
1116
use Magento\SalesRule\Api\CouponRepositoryInterface;
1217
use Magento\SalesRule\Model\Coupon;
1318
use Magento\SalesRule\Model\ResourceModel\Coupon\Usage;
1419
use Magento\SalesRule\Model\Rule\CustomerFactory;
1520
use Magento\SalesRule\Model\RuleFactory;
21+
use Magento\Framework\Lock\LockManagerInterface;
1622

1723
/**
24+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1825
* Processor to update coupon usage
1926
*/
2027
class Processor
2128
{
29+
/**
30+
* @var string
31+
*/
32+
private const LOCK_NAME = 'coupon_code_';
33+
34+
/**
35+
* @var string
36+
*/
37+
private const ERROR_MESSAGE = "coupon exceeds usage limit.";
38+
39+
/**
40+
* @var int
41+
*/
42+
private const LOCK_TIMEOUT = 60;
43+
44+
/**
45+
* @var LockManagerInterface
46+
*/
47+
private LockManagerInterface $lockManager;
48+
2249
/**
2350
* @param RuleFactory $ruleFactory
2451
* @param CustomerFactory $ruleCustomerFactory
2552
* @param Usage $couponUsage
2653
* @param CouponRepositoryInterface $couponRepository
2754
* @param SearchCriteriaBuilder $criteriaBuilder
55+
* @param LockManagerInterface|null $lockManager
2856
*/
2957
public function __construct(
3058
private readonly RuleFactory $ruleFactory,
3159
private readonly CustomerFactory $ruleCustomerFactory,
3260
private readonly Usage $couponUsage,
3361
private readonly CouponRepositoryInterface $couponRepository,
34-
private readonly SearchCriteriaBuilder $criteriaBuilder
62+
private readonly SearchCriteriaBuilder $criteriaBuilder,
63+
LockManagerInterface $lockManager = null
3564
) {
65+
$this->lockManager = $lockManager ?? ObjectManager::getInstance()->get(LockManagerInterface::class);
3666
}
3767

3868
/**
@@ -54,21 +84,77 @@ public function process(UpdateInfo $updateInfo): void
5484
/**
5585
* Update the number of coupon usages
5686
*
87+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
5788
* @param UpdateInfo $updateInfo
89+
* @throws CouldNotSaveException|LocalizedException
5890
*/
5991
public function updateCouponUsages(UpdateInfo $updateInfo): void
6092
{
61-
$isIncrement = $updateInfo->isIncrement();
6293
$coupons = $this->retrieveCoupons($updateInfo);
6394

6495
if ($updateInfo->isCouponAlreadyApplied()) {
6596
return;
6697
}
67-
98+
$incrementedCouponIds = [];
6899
foreach ($coupons as $coupon) {
69-
if ($updateInfo->isIncrement() || $coupon->getTimesUsed() > 0) {
70-
$coupon->setTimesUsed($coupon->getTimesUsed() + ($isIncrement ? 1 : -1));
71-
$coupon->save();
100+
$this->lockLoadedCoupon($coupon, $updateInfo, $incrementedCouponIds);
101+
$incrementedCouponIds[] = $coupon->getId();
102+
}
103+
}
104+
105+
/**
106+
* Lock loaded coupons
107+
*
108+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
109+
* @param Coupon $coupon
110+
* @param UpdateInfo $updateInfo
111+
* @param array $incrementedCouponIds
112+
* @return void
113+
* @throws CouldNotSaveException
114+
*/
115+
private function lockLoadedCoupon(Coupon $coupon, UpdateInfo $updateInfo, array $incrementedCouponIds): void
116+
{
117+
$isIncrement = $updateInfo->isIncrement();
118+
$lockName = self::LOCK_NAME . $coupon->getCode();
119+
if ($this->lockManager->lock($lockName, self::LOCK_TIMEOUT)) {
120+
try {
121+
$coupon = $this->couponRepository->getById($coupon->getId());
122+
123+
if ($updateInfo->isIncrement() && $coupon->getUsageLimit() &&
124+
$coupon->getTimesUsed() >= $coupon->getUsageLimit()) {
125+
126+
if (!empty($incrementedCouponIds)) {
127+
$this->revertCouponTimesUsed($incrementedCouponIds);
128+
}
129+
throw new CouldNotSaveException(__(sprintf('%s %s', $coupon->getCode(), self::ERROR_MESSAGE)));
130+
}
131+
132+
if ($updateInfo->isIncrement() || $coupon->getTimesUsed() > 0) {
133+
$coupon->setTimesUsed($coupon->getTimesUsed() + ($isIncrement ? 1 : -1));
134+
$coupon->save();
135+
}
136+
} finally {
137+
$this->lockManager->unlock($lockName);
138+
}
139+
}
140+
}
141+
142+
/**
143+
* Revert times_used of coupon if exception occurred for multiple applied coupon.
144+
*
145+
* @param array $incrementedCouponIds
146+
* @return void
147+
* @throws CouldNotSaveException|Exception
148+
*/
149+
private function revertCouponTimesUsed(array $incrementedCouponIds): void
150+
{
151+
foreach ($incrementedCouponIds as $couponId) {
152+
$coupon = $this->couponRepository->getById($couponId);
153+
$coupon->setTimesUsed($coupon->getTimesUsed() - 1);
154+
try {
155+
$this->couponRepository->save($coupon);
156+
} catch (Exception $e) {
157+
throw new CouldNotSaveException(__('Error occurred when saving coupon: %1', $e->getMessage()));
72158
}
73159
}
74160
}
@@ -130,7 +216,7 @@ public function updateCustomerRulesUsages(UpdateInfo $updateInfo): void
130216
* @param bool $isIncrement
131217
* @param int $ruleId
132218
* @param int $customerId
133-
* @throws \Exception
219+
* @throws Exception
134220
*/
135221
private function updateCustomerRuleUsages(bool $isIncrement, int $ruleId, int $customerId): void
136222
{
@@ -157,6 +243,7 @@ private function updateCustomerRuleUsages(bool $isIncrement, int $ruleId, int $c
157243
*/
158244
private function retrieveCoupons(UpdateInfo $updateInfo): array
159245
{
246+
160247
if (!$updateInfo->getCouponCode() && empty($updateInfo->getCouponCodes())) {
161248
return [];
162249
}

app/code/Magento/SalesRule/Test/Unit/Model/Coupon/Usage/ProcessorTest.php

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

1010
use Magento\Framework\Api\SearchCriteriaBuilder;
1111
use Magento\Framework\Api\SearchCriteriaInterface;
12+
use Magento\Framework\Lock\LockManagerInterface;
1213
use Magento\SalesRule\Api\CouponRepositoryInterface;
1314
use Magento\SalesRule\Api\Data\CouponSearchResultInterface;
1415
use Magento\SalesRule\Model\Coupon;
@@ -62,6 +63,11 @@ class ProcessorTest extends TestCase
6263
*/
6364
private $criteriaBuilder;
6465

66+
/**
67+
* @var LockManagerInterface|LockManagerInterface&MockObject|MockObject
68+
*/
69+
private $lockManager;
70+
6571
/**
6672
* @inheritDoc
6773
*/
@@ -76,13 +82,15 @@ protected function setUp(): void
7682
$this->criteriaBuilder->method('addFilter')->willReturnSelf();
7783
$searchCriteria = $this->createMock(SearchCriteriaInterface::class);
7884
$this->criteriaBuilder->method('create')->willReturn($searchCriteria);
85+
$this->lockManager = $this->createMock(LockManagerInterface::class);
7986

8087
$this->processor = new Processor(
8188
$this->ruleFactoryMock,
8289
$this->ruleCustomerFactoryMock,
8390
$this->couponUsageMock,
8491
$this->couponRepository,
85-
$this->criteriaBuilder
92+
$this->criteriaBuilder,
93+
$this->lockManager
8694
);
8795
}
8896

@@ -113,6 +121,7 @@ public function testProcess($isIncrement, $timesUsed): void
113121
->willReturn([$couponMock]);
114122
$this->couponRepository->method('getList')->willReturn($searchResult);
115123
$couponMock->expects($this->atLeastOnce())->method('getId')->willReturn($couponId);
124+
$this->couponRepository->method('getById')->with($couponId)->willReturn($couponMock);
116125
$couponMock->expects($this->atLeastOnce())->method('getTimesUsed')->willReturn($timesUsed);
117126
$couponMock->expects($this->any())->method('setTimesUsed')->with($setTimesUsed)->willReturnSelf();
118127
$couponMock->expects($this->any())->method('save')->willReturnSelf();
@@ -131,6 +140,9 @@ public function testProcess($isIncrement, $timesUsed): void
131140
->getMock();
132141
$customerRuleMock->expects($this->once())->method('loadByCustomerRule')->with($customerId, $ruleId)
133142
->willReturnSelf();
143+
144+
$this->lockManager->expects($this->any())->method('lock')->willReturn(true);
145+
$this->lockManager->expects($this->any())->method('unlock')->willReturn(true);
134146
$customerRuleMock->expects($this->once())->method('getId')->willReturn($ruleCustomerId);
135147
$customerRuleMock->expects($this->any())->method('getTimesUsed')->willReturn($timesUsed);
136148
$customerRuleMock->expects($this->any())->method('setTimesUsed')->willReturn($setTimesUsed);
@@ -158,7 +170,7 @@ public function testProcess($isIncrement, $timesUsed): void
158170
/**
159171
* @return array
160172
*/
161-
public function dataProvider(): array
173+
public static function dataProvider(): array
162174
{
163175
return [
164176
[true, 1],

app/code/Magento/SalesRule/i18n/en_US.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,4 @@ Apply,Apply
173173
"Coupon quantity should be less than or equal to the coupon quantity in the store configuration.","Coupon quantity should be less than or equal to the coupon quantity in the store configuration."
174174
"Code Quantity Limit","Code Quantity Limit"
175175
"For better performance max value allowed is 250,000. Set 0 to disable it.","For better performance max value allowed is 250,000. Set 0 to disable it."
176+
"coupon exceeds usage limit.","coupon exceeds usage limit."

0 commit comments

Comments
 (0)