PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.0
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / Coupon / DiscountService.php

DiscountService.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.0, at app/Services/Coupon/DiscountService.php

723 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Coupon;
4
5 use FluentCart\App\Helpers\Helper;
6 use FluentCart\App\Models\AppliedCoupon;
7 use FluentCart\App\Models\Cart;
8 use FluentCart\App\Models\Coupon;
9 use FluentCart\App\Models\Customer;
10 use FluentCart\Framework\Support\Arr;
11
12 class DiscountService
13 {
14 protected $cart = null;
15
16 protected $cartItems = [];
17
18 protected $customer = null;
19
20 protected $appliedCoupons = [];
21
22 protected $validCoupons = [];
23
24 protected $invalidCoupons = [];
25
26 protected $perCouponDiscounts = [];
27
28 public function __construct(?Cart $cart = null, $cartItems = [], $customer = null)
29 {
30 $this->cart = $cart;
31
32 if ($cartItems) {
33 $this->cartItems = $cartItems;
34 } else if ($cart) {
35 $this->cartItems = $cart->cart_data;
36 }
37
38 if ($customer) {
39 $this->customer = $customer;
40 }
41 }
42
43 public function resetIndividualItemsDiscounts()
44 {
45 foreach ($this->cartItems as &$item) {
46 $item['discount_total'] = Arr::get($item, 'manual_discount', 0);
47 $item['coupon_discount'] = 0;
48 $item['line_total'] = (int)($item['subtotal'] - $item['discount_total']);
49 if (isset($item['recurring_discounts'])) {
50 unset($item['recurring_discounts']);
51 }
52 }
53
54 $this->cartItems = array_values($this->cartItems);
55 $this->cart->cart_data = $this->cartItems;
56 $this->cart->save();
57 return $this;
58 }
59
60 public function revalidateCoupons()
61 {
62 if ($this->cart && $this->cart->coupons) {
63 return $this->applyCouponCodes($this->cart->coupons);
64 }
65
66 return new \WP_Error('no_coupons', __('No coupons found to revalidate.', 'fluent-cart'));
67 }
68
69 public function applyCouponCodes($codes = [])
70 {
71 if (!is_array($codes)) {
72 $codes = [$codes];
73 }
74
75 $existingCoupons = $this->cart ? $this->cart->coupons : [];
76
77 if (!$existingCoupons || !is_array($existingCoupons)) {
78 $existingCoupons = [];
79 }
80
81 $codes = array_merge($existingCoupons, $codes);
82
83 $codes = array_map('trim', $codes);
84 $codes = array_filter($codes);
85 $codes = array_unique($codes);
86 $codes = array_values($codes);
87
88 $coupons = Coupon::query()->whereIn('code', $codes)->get();
89
90 /*
91 * Allow addons to resolve codes that do not exist in fct_coupons into in-memory
92 * (virtual) Coupon models — e.g. a wallet / store-credit integration that applies a
93 * discount without persisting a coupon. The filter receives the DB-found coupons,
94 * the requested codes, and the cart; it may append unsaved Coupon instances.
95 */
96 $coupons = apply_filters('fluent_cart/coupon/resolve_coupons', $coupons, $codes, [
97 'cart' => $this->cart,
98 ]);
99
100 if ($coupons->isEmpty()) {
101 return new \WP_Error('no_valid_coupons', __('No matching coupon found for this code.', 'fluent-cart'), []);
102 }
103
104 $invalidCoupons = [];
105
106 $formattedCoupons = $this->formatCoupons($coupons, $codes);
107 $validCoupons = [];
108
109 foreach ($formattedCoupons as $coupon) {
110 $validCoupon = $this->isCouponValid($coupon);
111 if (is_wp_error($validCoupon)) {
112 $invalidCoupons[$coupon->code] = [
113 'error' => $validCoupon->get_error_message(),
114 'error_code' => $validCoupon->get_error_code()
115 ];
116 } else {
117 $validCoupons[] = $coupon;
118 }
119 }
120
121 if (empty($validCoupons)) {
122 $message = __('Coupon can not be applied.', 'fluent-cart');
123 if (!empty($invalidCoupons)) {
124 $firstInvalid = reset($invalidCoupons);
125 if (!empty($firstInvalid['error'])) {
126 $message = $firstInvalid['error'];
127 }
128 }
129 return new \WP_Error('no_valid_coupons', $message, $invalidCoupons);
130 }
131
132 // Let's check if we have multiple coupons and if they are stackable. If not, we will only keep the first one and invalidate the rest.
133 if (count($validCoupons) >= 2) {
134 $intermediateValidCoupons = [];
135 foreach ($validCoupons as $coupon) {
136 if ($coupon->stackable === 'yes') {
137 $intermediateValidCoupons[] = $coupon;
138 } else {
139 $invalidCoupons[$coupon->code] = [
140 'success' => false,
141 'error' => __('This coupon cannot be stacked with other coupons.', 'fluent-cart'),
142 'error_code' => 'coupon_not_stackable'
143 ];
144 }
145 }
146
147 if (!$intermediateValidCoupons) {
148 $validCoupons = [$validCoupons[0]];
149 } else {
150 $validCoupons = $intermediateValidCoupons;
151 }
152 }
153
154 // Ensure stackable coupons are applied in priority order (lower value = higher priority)
155 if (count($validCoupons) >= 2) {
156 usort($validCoupons, function ($a, $b) {
157 $priorityA = isset($a->priority) ? (int)$a->priority : 0;
158 $priorityB = isset($b->priority) ? (int)$b->priority : 0;
159
160 if ($priorityA === $priorityB) {
161 return 0;
162 }
163
164 return ($priorityA < $priorityB) ? -1 : 1;
165 });
166 }
167
168 // Now we have all the valid and stackable coupons. Let's apply them to the cart.
169 $this->resetIndividualItemsDiscounts();
170
171 foreach ($validCoupons as $index => $coupon) {
172 $result = $this->apply($coupon);
173 if (is_wp_error($result)) {
174 $invalidCoupons[$coupon->code] = [
175 'success' => false,
176 'error' => $result->get_error_message(),
177 'error_code' => $result->get_error_code()
178 ];
179 unset($validCoupons[$index]);
180 }
181 }
182
183 $this->validCoupons = $validCoupons;
184 $this->invalidCoupons = $invalidCoupons;
185
186 return $this->getResult();
187 }
188
189 public function getResult()
190 {
191 $couponResults = $this->invalidCoupons;
192
193 foreach ($this->validCoupons as $validCoupon) {
194 $couponResults[$validCoupon->code] = [
195 'success' => true,
196 'coupon' => $validCoupon
197 ];
198 }
199
200 return [
201 'applied_coupon_codes' => $this->appliedCoupons,
202 'coupon_results' => $couponResults,
203 'cart_items' => $this->cartItems,
204 'per_coupon_discounts' => $this->perCouponDiscounts
205 ];
206 }
207
208 public function getCartItems()
209 {
210 return $this->cartItems;
211 }
212
213 public function getPerCouponDiscounts()
214 {
215 return $this->perCouponDiscounts;
216 }
217
218 public function getAppliedCoupons()
219 {
220 return $this->appliedCoupons;
221 }
222
223 public function apply(Coupon $coupon)
224 {
225 $cartItems = apply_filters('fluent_cart/discount/pre_apply', $this->cartItems, [
226 'coupon' => $coupon,
227 'cart' => $this->cart,
228 ]);
229
230 $canUseCheck = $this->checkCanUseCoupon($coupon, $cartItems);
231 if (is_wp_error($canUseCheck)) {
232 return $canUseCheck;
233 }
234
235 $preValidatedItems = $this->filterApplicableItems($cartItems, $coupon);
236 if (empty($preValidatedItems)) {
237 return new \WP_Error('no_applicable_items', __('No applicable items found for this coupon.', 'fluent-cart'));
238 }
239
240 $currentItemsSubtotal = $this->calculateItemsSubtotal($preValidatedItems);
241 $currentItemsDiscountTotal = $this->calculateExistingCouponDiscount($preValidatedItems);
242 $currentItemsTotalAfterDiscount = $currentItemsSubtotal - $currentItemsDiscountTotal;
243
244 if ($currentItemsTotalAfterDiscount <= 0) {
245 return new \WP_Error('items_already_discounted', __('The eligible items are already fully discounted by another coupon.', 'fluent-cart'));
246 }
247
248 $percent = $this->calculateDiscountPercent($coupon, $currentItemsTotalAfterDiscount);
249
250 list($preValidatedItems, $couponDiscountTotal) = $this->applyDiscountToItems($preValidatedItems, $percent, $coupon);
251
252 if ($coupon->type === 'fixed') {
253 list($preValidatedItems, $couponDiscountTotal) = $this->correctFixedCouponRounding(
254 $preValidatedItems, $coupon, $couponDiscountTotal
255 );
256 }
257
258 $cartItems = $this->mergeValidatedItems($cartItems, $preValidatedItems);
259
260 if (!$couponDiscountTotal) {
261 return new \WP_Error('no_discount_applied', __('This coupon does not provide any additional discount on your order.', 'fluent-cart'));
262 }
263
264 $cartItems = $this->updateItemTotals($cartItems);
265
266 $this->cartItems = array_values($cartItems);
267 $this->appliedCoupons[] = $coupon->code;
268 $this->perCouponDiscounts[$coupon->code] = $couponDiscountTotal;
269
270 return true;
271 }
272
273 private function checkCanUseCoupon(Coupon $coupon, array $cartItems)
274 {
275 $canUse = apply_filters('fluent_cart/coupon/can_use_coupon', true, [
276 'coupon' => $coupon,
277 'cart' => $this->cart,
278 'cart_items' => $cartItems,
279 ]);
280
281 if (!$canUse || is_wp_error($canUse)) {
282 $message = __('This coupon is not available for your order.', 'fluent-cart');
283 if (is_wp_error($canUse)) {
284 $message = $canUse->get_error_message();
285 }
286 return new \WP_Error('coupon_cannot_be_used', $message);
287 }
288
289 return true;
290 }
291
292 private function filterApplicableItems(array $cartItems, Coupon $coupon)
293 {
294 $conditions = $coupon->conditions;
295
296 $filtered = array_filter($cartItems, function ($item) use ($coupon, $conditions) {
297 $willPreSkip = apply_filters('fluent_cart/coupon/will_skip_item', false, [
298 'item' => $item,
299 'coupon' => $coupon,
300 'cart' => $this->cart
301 ]);
302
303 if ($willPreSkip || Arr::get($item, 'other_info.is_locked') === 'yes') {
304 return false;
305 }
306
307 $excludedProducts = Arr::get($conditions, 'excluded_products', []);
308 if ($excludedProducts && in_array($item['object_id'], $excludedProducts)) {
309 return false;
310 }
311
312 $includedProducts = Arr::get($conditions, 'included_products', []);
313 if (!is_array($includedProducts)) {
314 $includedProducts = [];
315 }
316 if ($includedProducts && !in_array($item['object_id'], $includedProducts)) {
317 return false;
318 }
319
320 $includedCategories = Arr::get($conditions, 'included_categories', []);
321 if (!is_array($includedCategories)) {
322 $includedCategories = [];
323 }
324
325 $excludedCategories = Arr::get($conditions, 'excluded_categories', []);
326 if (!is_array($excludedCategories)) {
327 $excludedCategories = [];
328 }
329
330 if ($includedCategories || $excludedCategories) {
331 $productCategoryIds = $this->getProductCategories(Arr::get($item, 'post_id'));
332 if ($includedCategories) {
333 $intersect = array_intersect($includedCategories, $productCategoryIds);
334 if (empty($intersect)) {
335 return false;
336 }
337 }
338
339 if ($excludedCategories) {
340 $intersect = array_intersect($excludedCategories, $productCategoryIds);
341 if (!empty($intersect)) {
342 return false;
343 }
344 }
345 }
346
347 $emailRestrictions = trim(Arr::get($conditions, 'email_restrictions', ''));
348 if ($emailRestrictions) {
349 $customerEmail = $this->cart ? $this->cart->email : '';
350 if (!$customerEmail) {
351 return false;
352 }
353
354 $allowedEmails = array_filter(array_map('trim', explode(',', $emailRestrictions)));
355 if ($allowedEmails) {
356 foreach ($allowedEmails as $email) {
357 $pattern = '/^' . str_replace('\*', '.*', preg_quote($email, '/')) . '$/i';
358 if (preg_match($pattern, $customerEmail)) {
359 return true;
360 }
361 }
362
363 return false;
364 }
365 }
366
367 return true;
368 });
369
370 return array_values(array_filter($filtered));
371 }
372
373 private function calculateItemsSubtotal(array $items)
374 {
375 return array_sum(array_map(function ($item) {
376 return $this->getItemEffectiveSubtotal($item);
377 }, $items));
378 }
379
380 private function calculateExistingCouponDiscount(array $items)
381 {
382 return array_sum(array_map(function ($item) {
383 return (int) Arr::get($item, 'coupon_discount', 0);
384 }, $items));
385 }
386
387 private function calculateDiscountPercent(Coupon $coupon, $totalAfterDiscount)
388 {
389 if ($coupon->type == 'fixed') {
390 if ($coupon->amount >= $totalAfterDiscount) {
391 return 100.0;
392 }
393 return round(($coupon->amount / $totalAfterDiscount) * 100, 2);
394 }
395
396 return round(min(100, max(0, (float) $coupon->amount)), 2);
397 }
398
399 private function applyDiscountToItems(array $items, $percent, Coupon $coupon)
400 {
401 $couponDiscountTotal = 0;
402
403 foreach ($items as $index => $item) {
404 $existingAmount = (int) Arr::get($item, 'coupon_discount', 0);
405 $itemSubtotal = $this->getItemEffectiveSubtotal($item);
406 $hasTrialDays = Arr::get($item, 'other_info.payment_type') === 'subscription'
407 && Arr::get($item, 'other_info.trial_days', 0) > 0;
408
409 $remainingTotal = max(0, $itemSubtotal - $existingAmount);
410 $currentDiscount = (int) round($remainingTotal * ($percent / 100));
411 $discountTotal = min($existingAmount + $currentDiscount, $itemSubtotal);
412 $netDiscount = max(0, $discountTotal - $existingAmount);
413
414 $couponDiscountTotal += $netDiscount;
415 $items[$index]['coupon_discount'] = $discountTotal;
416
417 // Apply recurring discount for non-trial subscriptions
418 if (Arr::get($item, 'other_info.payment_type') === 'subscription' && !$hasTrialDays) {
419 if (!isset($items[$index]['recurring_discounts'])) {
420 $items[$index]['recurring_discounts'] = [
421 'signup' => 0,
422 'amount' => 0
423 ];
424 }
425
426 if ($coupon->isRecurringDiscount()) {
427 $unitPrice = (int) Arr::get($item, 'unit_price', 0);
428 if ($unitPrice > 0) {
429 $previousAmount = (int) Arr::get($item, 'recurring_discounts.amount', 0);
430 $remainingRecurring = max(0, $unitPrice - $previousAmount);
431 $recurringDiscount = (int) round($remainingRecurring * ($percent / 100));
432 $totalRecurringDiscount = min($previousAmount + $recurringDiscount, $unitPrice);
433
434 Arr::set($items, $index . '.recurring_discounts.amount', $totalRecurringDiscount);
435 }
436 }
437 }
438 }
439
440 return [$items, $couponDiscountTotal];
441 }
442
443 private function correctFixedCouponRounding(array $items, Coupon $coupon, $couponDiscountTotal)
444 {
445 if ($couponDiscountTotal < $coupon->amount) {
446 $remainingAmount = $coupon->amount - $couponDiscountTotal;
447 foreach ($items as $index => $item) {
448 if ($remainingAmount <= 0) {
449 break;
450 }
451
452 $subtotal = $this->getItemEffectiveSubtotal($item);
453 $maximumReduction = (int) ($subtotal - Arr::get($item, 'coupon_discount', 0));
454 if ($maximumReduction <= 0) {
455 continue;
456 }
457
458 $newDiscountAmount = min($maximumReduction, $remainingAmount);
459 $items[$index]['coupon_discount'] = Arr::get($item, 'coupon_discount', 0) + $newDiscountAmount;
460 $couponDiscountTotal += $newDiscountAmount;
461 $remainingAmount -= $newDiscountAmount;
462 }
463 } else if ($couponDiscountTotal > $coupon->amount) {
464 $excessAmount = $couponDiscountTotal - $coupon->amount;
465 foreach ($items as $index => $item) {
466 if ($excessAmount <= 0) {
467 break;
468 }
469
470 $existingDiscount = Arr::get($item, 'coupon_discount', 0);
471 if ($existingDiscount <= 0) {
472 continue;
473 }
474
475 $newReductionAmount = min($existingDiscount, $excessAmount);
476 $items[$index]['coupon_discount'] = $existingDiscount - $newReductionAmount;
477 $couponDiscountTotal -= $newReductionAmount;
478 $excessAmount -= $newReductionAmount;
479 }
480 }
481
482 return [$items, $couponDiscountTotal];
483 }
484
485 private function mergeValidatedItems(array $cartItems, array $validatedItems)
486 {
487 foreach ($cartItems as $index => $item) {
488 foreach ($validatedItems as $preItem) {
489 if ($item['id'] == $preItem['id']) {
490 $cartItems[$index] = $preItem;
491 break;
492 }
493 }
494 }
495
496 return $cartItems;
497 }
498
499 private function updateItemTotals(array $cartItems)
500 {
501 foreach ($cartItems as &$item) {
502 $item['discount_total'] = (int) (Arr::get($item, 'manual_discount', 0) + Arr::get($item, 'coupon_discount', 0));
503 $subtotal = $this->getItemEffectiveSubtotal($item);
504 $item['line_total'] = max(0, (int) ($subtotal - $item['discount_total']));
505 }
506
507 return $cartItems;
508 }
509
510 private function getItemEffectiveSubtotal(array $item)
511 {
512 if (Arr::get($item, 'other_info.payment_type') === 'subscription'
513 && Arr::get($item, 'other_info.trial_days', 0) > 0
514 ) {
515 $quantity = max(1, (int) Arr::get($item, 'quantity', 1));
516 // When dynamic RC has already adjusted signup_fee to the net amount, use the
517 // pre-adjustment gross value so the coupon always applies to the original price.
518 $signupFee = Arr::get($item, 'other_info.original_signup_fee') !== null
519 ? (int) Arr::get($item, 'other_info.original_signup_fee')
520 : (int) Arr::get($item, 'other_info.signup_fee', 0);
521 return $signupFee * $quantity;
522 }
523
524 // When dynamic RC has already reduced unit_price to the net (tax-stripped) amount,
525 // use the saved gross price so the coupon is always calculated against the original
526 // inclusive price — regardless of whether VAT number was entered before or after
527 // the coupon was applied.
528 $originalUnitPrice = Arr::get($item, 'line_meta.original_unit_price');
529 if ($originalUnitPrice !== null) {
530 $quantity = max(1, (int) Arr::get($item, 'quantity', 1));
531 return (int) $originalUnitPrice * $quantity;
532 }
533
534 return (int) $item['subtotal'];
535 }
536
537 public function saveCart()
538 {
539 if (!$this->cart) {
540 return new \WP_Error('no_cart', __('No cart found to save.', 'fluent-cart'));
541 }
542
543 $existingCheckoutData = $this->cart->checkout_data;
544
545 if (!is_array($existingCheckoutData)) {
546 $existingCheckoutData = [];
547 }
548
549 $existingCheckoutData['__per_coupon_discounts'] = $this->perCouponDiscounts;
550
551 $this->cart->cart_data = $this->cartItems;
552 $this->cart->coupons = $this->appliedCoupons;
553 $this->cart->save();
554 return $this->cart;
555 }
556
557 protected function formatCoupons($coupons, $codes)
558 {
559 $coupons = $coupons->keyBy('code');
560 $formatted = [];
561
562 foreach ($codes as $code) {
563 if (isset($coupons[$code])) {
564 $formatted[] = $coupons[$code];
565 }
566 }
567
568 return $formatted;
569 }
570
571 protected function isCouponValid($coupon)
572 {
573 $status = $coupon->status;
574 if ($status === 'expired') {
575 return new \WP_Error('coupon_expired', __('This coupon has expired.', 'fluent-cart'));
576 }
577 if ($status === 'scheduled') {
578 return new \WP_Error('coupon_not_started', __('This coupon is not yet active.', 'fluent-cart'));
579 }
580 if ($status !== 'active') {
581 return new \WP_Error('coupon_not_available', __('This coupon is not currently available.', 'fluent-cart'));
582 }
583
584 // let's validate the start date and end date first
585 $startDate = $coupon->start_date;
586 if ($startDate && $startDate != '0000-00-00 00:00:00' && strtotime($startDate) > time()) {
587 return new \WP_Error('coupon_not_started', __('This coupon is not yet active.', 'fluent-cart'));
588 }
589 $endDate = $coupon->end_date;
590 if ($endDate && $endDate != '0000-00-00 00:00:00' && strtotime($endDate) < time()) {
591 return new \WP_Error('coupon_expired', __('This coupon has expired.', 'fluent-cart'));
592 }
593
594 $conditions = $coupon->conditions;
595
596 // The spend limits below (min/max) are measured against either the cart subtotal
597 // (items only) or the full order total (shipping + fees included), per the coupon's
598 // min_amount_basis setting. Coupons created before this setting existed have no stored
599 // value and historically compared against the order total, so the fallback stays 'total'
600 // to preserve their behavior. New coupons default to 'subtotal' in the admin UI.
601 $amountBasis = Arr::get($conditions, 'min_amount_basis', 'total');
602
603 // add check max_purchase_amount
604 $maxPurchaseAmount = Arr::get($conditions, 'max_purchase_amount', 0);
605 $getCartTotal = 0;
606 if ($this->cart) {
607 $cartAmount = $amountBasis === 'total'
608 ? $this->cart->getEstimatedTotal()
609 : $this->cart->getItemsSubtotal();
610 $getCartTotal = ($cartAmount / 100);
611 }
612
613 if ($maxPurchaseAmount) {
614 if ($getCartTotal > $maxPurchaseAmount) {
615 return new \WP_Error('max_purchase_amount_exceeded', __('Your cart total exceeds the maximum amount allowed for this coupon.', 'fluent-cart'));
616 }
617 }
618
619 $minPurchaseAmount = Arr::get($conditions, 'min_purchase_amount', 0);
620 if ($minPurchaseAmount) {
621 if ($getCartTotal < ($minPurchaseAmount / 100)) {
622 return new \WP_Error('min_purchase_amount_not_met', __('Your cart total is below the minimum required to use this coupon.', 'fluent-cart'));
623 }
624 }
625
626 // Let's check the use count and max uses
627 $useCount = $coupon->use_count;
628 $maxUses = Arr::get($conditions, 'max_uses', 0);
629 if ($useCount && $maxUses && $useCount >= $maxUses) {
630 return new \WP_Error('coupon_max_uses_exceeded', __('This coupon has reached its maximum number of uses.', 'fluent-cart'));
631 }
632 $maxPerCustomer = Arr::get($conditions, 'max_per_customer', 0);
633 if ($maxPerCustomer) {
634 if (!is_user_logged_in()) {
635 return new \WP_Error('coupon_login_required', __('Please log in to use this coupon.', 'fluent-cart'));
636 }
637
638 $customer = $this->resolveCustomerForUsageLimit();
639 if ($customer) {
640 $usageQuery = AppliedCoupon::query()
641 ->where('coupon_id', $coupon->id)
642 ->whereHas('order', function ($orderQuery) use ($customer) {
643 $orderQuery->where('customer_id', $customer->id);
644 });
645
646 $usageQuery = apply_filters('fluent_cart/coupon/per_customer_usage_query', $usageQuery, [
647 'coupon' => $coupon,
648 'customer' => $customer,
649 'cart' => $this->cart,
650 ]);
651
652 $usedCount = $usageQuery->count();
653
654 if ($usedCount >= $maxPerCustomer) {
655 return new \WP_Error('coupon_max_uses_exceeded', __('You have already used this coupon the maximum number of times.', 'fluent-cart'));
656 }
657 }
658 }
659
660 return $coupon;
661 }
662
663 protected function resolveCustomerForUsageLimit()
664 {
665 if (!is_user_logged_in()) {
666 return null;
667 }
668
669 $customer = $this->getCustomer();
670 if ($customer) {
671 return $customer;
672 }
673
674 $customer = Customer::query()->where('user_id', get_current_user_id())->first();
675 if ($customer) {
676 $this->customer = $customer;
677 return $customer;
678 }
679
680 return null;
681 }
682
683 public function setCustomer(Customer $customer)
684 {
685 $this->customer = $customer;
686 }
687
688 public function getCustomer()
689 {
690 if ($this->customer) {
691 return $this->customer;
692 }
693
694 if ($this->cart) {
695 $this->customer = $this->cart->guessCustomer();
696 return $this->customer;
697 }
698
699 return null;
700 }
701
702 protected function getProductCategories($postId)
703 {
704 static $cached = [];
705
706 if (isset($cached[$postId])) {
707 return $cached[$postId];
708 }
709
710
711 $taxonomyName = 'product-categories';
712 $terms = get_the_terms($postId, $taxonomyName);
713 if (is_wp_error($terms) || !$terms) {
714 $cached[$postId] = [];
715 } else {
716 $cached[$postId] = wp_list_pluck($terms, 'term_id');
717 }
718
719 return $cached[$postId];
720 }
721
722 }
723