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

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