77
88namespace Magento \SalesRule \Model \Coupon \Usage ;
99
10+ use Exception ;
1011use 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 ;
1116use Magento \SalesRule \Api \CouponRepositoryInterface ;
1217use Magento \SalesRule \Model \Coupon ;
1318use Magento \SalesRule \Model \ResourceModel \Coupon \Usage ;
1419use Magento \SalesRule \Model \Rule \CustomerFactory ;
1520use Magento \SalesRule \Model \RuleFactory ;
21+ use Magento \Framework \Lock \LockManagerInterface ;
1622
1723/**
24+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1825 * Processor to update coupon usage
1926 */
2027class 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 }
0 commit comments