7
7
8
8
namespace Magento \SalesRule \Model \Coupon \Usage ;
9
9
10
+ use Exception ;
10
11
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 ;
11
16
use Magento \SalesRule \Api \CouponRepositoryInterface ;
12
17
use Magento \SalesRule \Model \Coupon ;
13
18
use Magento \SalesRule \Model \ResourceModel \Coupon \Usage ;
14
19
use Magento \SalesRule \Model \Rule \CustomerFactory ;
15
20
use Magento \SalesRule \Model \RuleFactory ;
21
+ use Magento \Framework \Lock \LockManagerInterface ;
16
22
17
23
/**
24
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
18
25
* Processor to update coupon usage
19
26
*/
20
27
class Processor
21
28
{
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
+
22
49
/**
23
50
* @param RuleFactory $ruleFactory
24
51
* @param CustomerFactory $ruleCustomerFactory
25
52
* @param Usage $couponUsage
26
53
* @param CouponRepositoryInterface $couponRepository
27
54
* @param SearchCriteriaBuilder $criteriaBuilder
55
+ * @param LockManagerInterface|null $lockManager
28
56
*/
29
57
public function __construct (
30
58
private readonly RuleFactory $ ruleFactory ,
31
59
private readonly CustomerFactory $ ruleCustomerFactory ,
32
60
private readonly Usage $ couponUsage ,
33
61
private readonly CouponRepositoryInterface $ couponRepository ,
34
- private readonly SearchCriteriaBuilder $ criteriaBuilder
62
+ private readonly SearchCriteriaBuilder $ criteriaBuilder ,
63
+ LockManagerInterface $ lockManager = null
35
64
) {
65
+ $ this ->lockManager = $ lockManager ?? ObjectManager::getInstance ()->get (LockManagerInterface::class);
36
66
}
37
67
38
68
/**
@@ -54,21 +84,77 @@ public function process(UpdateInfo $updateInfo): void
54
84
/**
55
85
* Update the number of coupon usages
56
86
*
87
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
57
88
* @param UpdateInfo $updateInfo
89
+ * @throws CouldNotSaveException|LocalizedException
58
90
*/
59
91
public function updateCouponUsages (UpdateInfo $ updateInfo ): void
60
92
{
61
- $ isIncrement = $ updateInfo ->isIncrement ();
62
93
$ coupons = $ this ->retrieveCoupons ($ updateInfo );
63
94
64
95
if ($ updateInfo ->isCouponAlreadyApplied ()) {
65
96
return ;
66
97
}
67
-
98
+ $ incrementedCouponIds = [];
68
99
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 ()));
72
158
}
73
159
}
74
160
}
@@ -130,7 +216,7 @@ public function updateCustomerRulesUsages(UpdateInfo $updateInfo): void
130
216
* @param bool $isIncrement
131
217
* @param int $ruleId
132
218
* @param int $customerId
133
- * @throws \ Exception
219
+ * @throws Exception
134
220
*/
135
221
private function updateCustomerRuleUsages (bool $ isIncrement , int $ ruleId , int $ customerId ): void
136
222
{
@@ -157,6 +243,7 @@ private function updateCustomerRuleUsages(bool $isIncrement, int $ruleId, int $c
157
243
*/
158
244
private function retrieveCoupons (UpdateInfo $ updateInfo ): array
159
245
{
246
+
160
247
if (!$ updateInfo ->getCouponCode () && empty ($ updateInfo ->getCouponCodes ())) {
161
248
return [];
162
249
}
0 commit comments