PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
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.5.1, at app/Services/Coupon/DiscountService.php

713 lines 25.1 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 // add check max_purchase_amount
597 $maxPurchaseAmount = Arr::get($conditions, 'max_purchase_amount', 0);
598 $getCartTotal = 0;
599 if ($this->cart) {
600 $getCartTotal = ($this->cart->getEstimatedTotal() / 100);
601 }
602
603 if ($maxPurchaseAmount) {
604 if ($getCartTotal > $maxPurchaseAmount) {
605 return new \WP_Error('max_purchase_amount_exceeded', __('Your cart total exceeds the maximum amount allowed for this coupon.', 'fluent-cart'));
606 }
607 }
608
609 $minPurchaseAmount = Arr::get($conditions, 'min_purchase_amount', 0);
610 if ($minPurchaseAmount) {
611 if ($getCartTotal < ($minPurchaseAmount / 100)) {
612 return new \WP_Error('min_purchase_amount_not_met', __('Your cart total is below the minimum required to use this coupon.', 'fluent-cart'));
613 }
614 }
615
616 // Let's check the use count and max uses
617 $useCount = $coupon->use_count;
618 $maxUses = Arr::get($conditions, 'max_uses', 0);
619 if ($useCount && $maxUses && $useCount >= $maxUses) {
620 return new \WP_Error('coupon_max_uses_exceeded', __('This coupon has reached its maximum number of uses.', 'fluent-cart'));
621 }
622 $maxPerCustomer = Arr::get($conditions, 'max_per_customer', 0);
623 if ($maxPerCustomer) {
624 if (!is_user_logged_in()) {
625 return new \WP_Error('coupon_login_required', __('Please log in to use this coupon.', 'fluent-cart'));
626 }
627
628 $customer = $this->resolveCustomerForUsageLimit();
629 if ($customer) {
630 $usageQuery = AppliedCoupon::query()
631 ->where('coupon_id', $coupon->id)
632 ->whereHas('order', function ($orderQuery) use ($customer) {
633 $orderQuery->where('customer_id', $customer->id);
634 });
635
636 $usageQuery = apply_filters('fluent_cart/coupon/per_customer_usage_query', $usageQuery, [
637 'coupon' => $coupon,
638 'customer' => $customer,
639 'cart' => $this->cart,
640 ]);
641
642 $usedCount = $usageQuery->count();
643
644 if ($usedCount >= $maxPerCustomer) {
645 return new \WP_Error('coupon_max_uses_exceeded', __('You have already used this coupon the maximum number of times.', 'fluent-cart'));
646 }
647 }
648 }
649
650 return $coupon;
651 }
652
653 protected function resolveCustomerForUsageLimit()
654 {
655 if (!is_user_logged_in()) {
656 return null;
657 }
658
659 $customer = $this->getCustomer();
660 if ($customer) {
661 return $customer;
662 }
663
664 $customer = Customer::query()->where('user_id', get_current_user_id())->first();
665 if ($customer) {
666 $this->customer = $customer;
667 return $customer;
668 }
669
670 return null;
671 }
672
673 public function setCustomer(Customer $customer)
674 {
675 $this->customer = $customer;
676 }
677
678 public function getCustomer()
679 {
680 if ($this->customer) {
681 return $this->customer;
682 }
683
684 if ($this->cart) {
685 $this->customer = $this->cart->guessCustomer();
686 return $this->customer;
687 }
688
689 return null;
690 }
691
692 protected function getProductCategories($postId)
693 {
694 static $cached = [];
695
696 if (isset($cached[$postId])) {
697 return $cached[$postId];
698 }
699
700
701 $taxonomyName = 'product-categories';
702 $terms = get_the_terms($postId, $taxonomyName);
703 if (is_wp_error($terms) || !$terms) {
704 $cached[$postId] = [];
705 } else {
706 $cached[$postId] = wp_list_pluck($terms, 'term_id');
707 }
708
709 return $cached[$postId];
710 }
711
712 }
713