PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / OrderService.php

OrderService.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Services/OrderService.php

695 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;
4
5 use FluentCart\Api\ModuleSettings;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\App;
8 use FluentCart\App\Helpers\AddressHelper;
9 use FluentCart\App\Helpers\Helper;
10 use FluentCart\App\Helpers\Status;
11 use FluentCart\App\Models\Model;
12 use FluentCart\App\Models\Order;
13 use FluentCart\App\Models\OrderItem;
14 use FluentCart\App\Models\OrderMeta;
15 use FluentCart\App\Models\OrderTransaction;
16 use FluentCart\App\Models\ProductVariation;
17 use FluentCart\App\Models\Subscription;
18 use FluentCart\App\Modules\Subscriptions\Services\SystemChargeService;
19 use FluentCart\App\Services\Payments\PaymentHelper;
20 use FluentCart\Framework\Support\Arr;
21
22 class OrderService
23 {
24 /**
25 * Group address and other data from the order data array.
26 *
27 * @param array $order_data
28 * @return array
29 */
30 public static function groupSanitizedData($order_data = []): array
31 {
32 // fct_billing_tax_id is the checkout field name for the VAT number; normalise it to
33 // billing_vat_number so extractAddressData maps it into billing_address.vat_number
34 if (isset($order_data['fct_billing_tax_id'])) {
35 $order_data['billing_vat_number'] = $order_data['fct_billing_tax_id'];
36 unset($order_data['fct_billing_tax_id']);
37 }
38
39 $billingAddress = static::extractAddressData($order_data, 'billing_');
40 $shippingAddress = static::extractAddressData($order_data, 'shipping_');
41 $others = static::extractOtherData($order_data);
42
43 if (Arr::get($others, 'ship_to_different', 'no') === 'no') {
44 $shippingAddress = $billingAddress;
45 }
46
47 $billingAddress = static::finalizeAddress($billingAddress, 'billing');
48 $shippingAddress = static::finalizeAddress($shippingAddress, 'shipping');
49
50 return [
51 'billing_address' => $billingAddress,
52 'shipping_address' => $shippingAddress,
53 'others' => $others,
54 ];
55 }
56
57 private static function extractAddressData($data, $prefix): array
58 {
59 $address = [];
60 foreach ($data as $key => $value) {
61 if (strpos($key, $prefix) === 0) {
62 $newKey = str_replace($prefix, '', $key);
63 $address[$newKey] = static::sanitizeEmailOrText($newKey, $value);
64 }
65 }
66 return $address;
67 }
68
69 private static function extractOtherData($data): array
70 {
71 $others = [];
72 foreach ($data as $key => $value) {
73 if (strpos($key, 'billing_') !== 0 && strpos($key, 'shipping_') !== 0) {
74 $others[$key] = sanitize_text_field($value);
75 }
76 }
77
78 return $others;
79 }
80
81 private static function finalizeAddress($address, $type): array
82 {
83 $fullName = Arr::get($address, 'full_name', '');
84 $name = Arr::get($address, 'name', '');
85 $address['full_name'] = $fullName ?? $name;
86 $address['first_name'] = AddressHelper::guessFirstNameAndLastName($address['full_name']);
87 $address['type'] = $type;
88 $name = $fullName ?? $name;
89 $address = array_merge($address, AddressHelper::guessFirstNameAndLastName($name));
90 $address['name'] = Arr::get($address, 'first_name', '') . ' ' . Arr::get($address, 'last_name', '');
91 return $address;
92 }
93
94
95 /**
96 * Sanitize email or text field based on the key.
97 *
98 * @param string $key
99 * @param string $value
100 * @return string
101 */
102 public static function sanitizeEmailOrText($key, $value): string
103 {
104 return $key === 'email' ? sanitize_email($value ?? '') : sanitize_text_field($value ?? '');
105 }
106
107 /**
108 * Pluck product IDs from the order.
109 *
110 * @param Order $order
111 * @return array
112 */
113 public static function pluckProductIds(Order $order): array
114 {
115 return array_column($order->order_items->toArray(), 'post_id');
116 }
117
118
119 /**
120 * Validate products and check if they are available by stock.
121 *
122 * @param array $products
123 * @return void
124 * @throws \Exception
125 */
126 public static function validateProducts($products,$prevOrder = null)
127 {
128 $itemIds = array_column($products, 'object_id');
129 $currentVariations = ProductVariation::query()
130 ->whereIn('id', $itemIds)
131 ->with(['product.detail', 'product_detail'])
132 ->get()
133 ->keyBy('id')
134 ->toArray();
135
136 foreach ($products as $product) {
137 // Custom item validation occurs during cart insertion.
138 // If the validation hook is missing or fails, the item is not added to the cart
139 // and therefore never reaches OrderService.
140 // Adding a fallback here would duplicate responsibility and break cart invariants.
141 // is_custom is already normalized and validated at cart level.
142 // No coercion required here.
143 if(!Arr::get($product, 'is_custom', false)) {
144 static::validateProductAvailability($product, $currentVariations);
145 static::validateSubscriptionQuantity($product);
146 static::validateStockStatus($product, $currentVariations, $prevOrder);
147 static::validateStockQuantity($product, $currentVariations, $prevOrder);
148 }
149 }
150 }
151
152 private static function validateProductAvailability($product, $currentVariations)
153 {
154 $currentVariation = $currentVariations[$product['object_id']] ?? null;
155
156 if (
157 empty($currentVariation) ||
158 empty($currentVariation['product']) ||
159 !in_array($currentVariation['product']['post_status'], ['publish', 'private'])
160 ) {
161 throw new \Exception(sprintf(
162 /* translators: %s: product title */
163 esc_html__('[%s] is not available.', 'fluent-cart'),
164 esc_html(Arr::get($product, 'title'))
165 ));
166 }
167 }
168
169 private static function validateSubscriptionQuantity($product)
170 {
171 $paymentType = Arr::get($product, 'other_info.payment_type', 'onetime');
172
173 if ($paymentType === 'subscription' && Arr::get($product, 'quantity') > 1) {
174 throw new \Exception(
175 sprintf(
176 /* translators: %s: product title */
177 esc_html__('You cannot purchase multiple quantities of the subscription product [%s]. Please adjust the quantity to 1.', 'fluent-cart'),
178 esc_html(Arr::get($product, 'title'))
179 )
180 );
181 }
182 }
183
184 private static function validateStockStatus($product, $currentVariations, $prevOrder = null)
185 {
186 if (!ModuleSettings::isActive('stock_management')) {
187 return;
188 }
189
190 $currentVariation = $currentVariations[$product['object_id']] ?? null;
191
192 // check if $prevOrder is not null.
193 if ($prevOrder) {
194
195 // get order_items from OrderItems
196 $orderItems = $prevOrder->order_items;
197
198 // filter payment_type is not signup_fee or fee
199 $orderItems = $orderItems->filter(function ($item) {
200 return !in_array($item->payment_type, ['signup_fee', 'fee']);
201 });
202
203 $stockMovement = OrderMeta::where('order_id', $prevOrder->id)
204 ->where('meta_key', 'stock_movement')
205 ->value('meta_value');
206
207 // Decode stock movement if it's JSON
208 $stockMovementData = $stockMovement ?? [];
209
210 // Check if the current product variation is already on hold in stock movement
211 $isStockOnHold = false;
212
213 if (!empty($stockMovementData)) {
214
215 // Find the order item that matches this variation ID
216 foreach ($orderItems as $item) {
217 if ($item->object_id == Arr::get($currentVariation, 'id')) {
218 $orderItemId = $item->id;
219
220 // Check if this order item has stock on hold
221 // Structure: {"95":{"committed":10,"on_hold":0}} where 95 is order item ID
222 if (isset($stockMovementData[$orderItemId]['on_hold']) &&
223 $stockMovementData[$orderItemId]['on_hold'] > 0) {
224 $isStockOnHold = true;
225 break;
226 }
227 }
228 }
229 }
230
231
232 // If stock is already on hold for this variation, skip stock validation
233 if ($isStockOnHold) {
234 // Stock is already reserved for this order, no need to check
235 return;
236 }
237
238 // If stock is NOT on hold, perform normal stock validation
239 if (
240 Arr::get($currentVariation, 'stock_status') !== "in-stock" &&
241 Arr::get($currentVariation, 'manage_stock') == 1
242 ) {
243 throw new \Exception(sprintf(
244 /* translators: %s: product title */
245 esc_html__('[%s] is out of stock.', 'fluent-cart'),
246 esc_html(Arr::get($product, 'title'))
247 ));
248 }
249
250 } else {
251
252 // No previous order, perform normal stock validation
253 if (
254 Arr::get($currentVariation, 'stock_status') !== "in-stock" &&
255 Arr::get($currentVariation, 'manage_stock') == 1
256 ) {
257 throw new \Exception(sprintf(
258 /* translators: %s: product title */
259 esc_html__('[%s] is out of stock.', 'fluent-cart'),
260 esc_html(Arr::get($product, 'title'))
261 ));
262 }
263 }
264 }
265
266 private static function validateStockQuantity($product, $currentVariations, $prevOrder = null)
267 {
268 if (!ModuleSettings::isActive('stock_management')) {
269 return;
270 }
271 $currentVariation = $currentVariations[$product['object_id']] ?? null;
272 $productQuantity = (int)Arr::get($product, 'quantity');
273
274 // check if $prevOrder is not null
275 if ($prevOrder) {
276
277 // get order_items from OrderItems
278 $orderItems = $prevOrder->order_items;
279
280
281 $orderItems = $orderItems->filter(function ($item) {
282 return !in_array($item->payment_type, ['signup_fee', 'fee']);
283 });
284
285 // get stock movement for this order
286 $stockMovement = OrderMeta::where('order_id', $prevOrder->id)
287 ->where('meta_key', 'stock_movement')
288 ->value('meta_value');
289
290 // Decode stock movement if it's JSON
291 $stockMovementData = $stockMovement ?? [];
292
293 // Check if the current product variation is already on hold in stock movement
294 $isStockOnHold = false;
295
296 if (!empty($stockMovementData)) {
297
298 // Find the order item that matches this variation ID
299 foreach ($orderItems as $item) {
300 if ($item->object_id == Arr::get($currentVariation, 'id')) {
301 $orderItemId = $item->id;
302
303 // Check if this order item has stock on hold
304 // Structure: {"95":{"committed":10,"on_hold":0}} where 95 is order item ID
305 if (isset($stockMovementData[$orderItemId]['on_hold']) &&
306 $stockMovementData[$orderItemId]['on_hold'] > 0) {
307 $isStockOnHold = true;
308 break;
309 }
310 }
311 }
312 }
313
314 // If stock is already on hold for this variation, skip quantity validation
315 if ($isStockOnHold) {
316 // Stock is already reserved for this order, no need to check quantity
317 return;
318 }
319 }
320
321 // Perform normal quantity validation if no previous order OR stock is not on hold
322 if (!static::allowItemToOrder($currentVariation, $productQuantity)) {
323 $note = sprintf(
324 /* translators: %s: product title */
325 esc_html__('[%1$s] is out of stock. Only %2$s left.', 'fluent-cart'),
326 esc_html(Arr::get($product, 'title')),
327 esc_html(Arr::get($currentVariation, 'available'))
328 );
329 throw new \Exception(esc_html($note));
330 }
331 }
332
333 /**
334 * Check if the item can be ordered based on stock availability.
335 *
336 * @param array $variation
337 * @param int $updatedQuantity
338 * @return bool
339 */
340 private static function allowItemToOrder($variation, $updatedQuantity): bool
341 {
342 if (Arr::get($variation, 'manage_stock') == 0) {
343 return true;
344 }
345 return $updatedQuantity <= Arr::get($variation, 'available');
346 }
347
348 /**
349 * Get the total amount of items without discount.
350 *
351 * @param array $orderItems
352 * @return float
353 */
354 public static function getItemsAmountWithoutDiscount(array $orderItems)
355 {
356 $total = 0;
357 foreach ($orderItems as $orderItem) {
358 $paymentType = Arr::get($orderItem, 'payment_type');
359 if (!$paymentType) {
360 $paymentType = Arr::get($orderItem, 'other_info.payment_type');
361 }
362
363 $total += self::calculateItemTotal($orderItem, $paymentType);
364
365 }
366 return $total;
367 }
368
369 private static function isDiscountedSubscription($orderItem): bool
370 {
371 $paymentType = Arr::get($orderItem, 'other_info.payment_type', '');
372 return $paymentType === 'subscription' && Arr::get($orderItem, 'discount_total', 0) > 0;
373 }
374
375 private static function isPlanChangeAdjustment($orderItem, $paymentType): bool
376 {
377 $orderType = Arr::get($orderItem, 'other_info.order_type', '');
378 return $orderType === 'plan_change' && $paymentType === 'adjustment';
379 }
380
381 private static function calculateItemTotal($orderItem, $paymentType): float
382 {
383
384 if (in_array($paymentType, ['signup_fee'], true)) {
385 return floatval($orderItem['unit_price']);
386 }
387
388 if ($paymentType == 'subscription') {
389 if ((int)Arr::get($orderItem, 'other_info.trial_days', 0) > 0) {
390 return (int)Arr::get($orderItem, 'other_info.signup_fee', 0);
391 }
392 return (int) Arr::get($orderItem, 'subtotal', intval($orderItem['unit_price'] * $orderItem['quantity'])) + (int)Arr::get($orderItem, 'other_info.signup_fee', 0);
393 }
394
395 return (int) Arr::get($orderItem, 'subtotal', intval($orderItem['unit_price'] * $orderItem['quantity']));
396 }
397
398 /**
399 * Get the total amount of items.
400 *
401 * @param array|Model $items
402 * @param bool $formatted
403 * @param bool $withCurrency
404 * @return float|string
405 */
406 public static function getItemsAmountTotal($items = [], $formatted = true, $withCurrency = true, $shippingTotal = 0)
407 {
408 $items = $items instanceof Model ? $items->toArray() : $items;
409
410 $total = 0;
411
412 foreach ($items as $cartItem) {
413 // according to cart structure, line total is item_price * quantity
414 $subtotal = floatval(Arr::get($cartItem, 'subtotal', 0));
415 $discountTotal = floatval(Arr::get($cartItem, 'discount_total', 0));
416
417 $otherInfo = Arr::get($cartItem, 'other_info', []);
418 if (is_object($otherInfo)) {
419 $otherInfo = (array)$otherInfo;
420 }
421
422 $trialDays = intval(Arr::get($otherInfo, 'trial_days', 0));
423 $signupFee = floatval(Arr::get($otherInfo, 'signup_fee', 0));
424
425 if ($trialDays > 0) {
426 // For subscriptions with trial days, charge only signup fee minus discount
427 // The discount_total is the discount applied to the signup fee
428 $total += max(0,$signupFee - $discountTotal);
429 continue;
430 }
431
432
433 $total += $subtotal - $discountTotal;
434
435
436 $total += floatval($signupFee - Arr::get($otherInfo, 'signup_discount', 0));
437 }
438
439 $total += $shippingTotal;
440
441 $total = apply_filters('fluent_cart/cart/items_total', $total, [
442 'items' => $items,
443 'shipping_total' => $shippingTotal,
444 ]);
445
446 // Ensure filtered total is non-negative
447 $total = max(0, (int)round((float)$total));
448
449 return $formatted ? Helper::toDecimal($total, $withCurrency) : $total;
450 }
451
452 private static function calculateItemAmount(array $cartItem, $price, $paymentType, $formatted, $withCurrency)
453 {
454 if ($paymentType === 'signup_fee') {
455 return $price;
456 }
457 if ($paymentType === 'adjustment') {
458 return $formatted ? Helper::toDecimal(intval($price), $withCurrency) : intval($price);
459 }
460 $quantity = intval(Arr::get($cartItem, 'quantity', 1));
461 $discount = floatval(Arr::get($cartItem, 'discount_total', 0));
462 return (($price * $quantity) - $discount);
463 }
464
465 /**
466 * Create line items from order items.
467 *
468 * @param array $orderItems
469 * @return array
470 */
471 public static function makeLineItemsFromOrderItems($orderItems): array
472 {
473 $subscriptionItems = [];
474 $items = [];
475
476 foreach ($orderItems as $orderItem) {
477 $orderItem = $orderItem instanceof Model ? $orderItem->toArray() : $orderItem;
478 if (static::isSubscriptionItem($orderItem)) {
479 $subscriptionItems[] = static::prepareSubscriptionItem($orderItem);
480 } else {
481 $items[] = static::prepareRegularItem($orderItem);
482 }
483 }
484
485 return [
486 'items' => $items,
487 'subscriptionItems' => $subscriptionItems
488 ];
489 }
490
491 private static function isSubscriptionItem(array $orderItem): bool
492 {
493 return Arr::get($orderItem, 'payment_type') === 'subscription';
494 }
495
496 private static function prepareSubscriptionItem(array $orderItem): array
497 {
498 $orderItem['other_info'] = Arr::get($orderItem, 'other_info', []);
499
500 $otherInfo = (array)Arr::get($orderItem, 'other_info');
501
502 return [
503 'product_id' => Arr::get($orderItem, 'post_id'),
504 'object_id' => Arr::get($orderItem, 'object_id', ''),
505 'billing_interval' => Arr::get($otherInfo, 'repeat_interval'),
506 'signup_fee' => Arr::get($otherInfo, 'signup_fee'),
507 'bill_times' => Arr::get($otherInfo, 'times'),
508 'recurring_amount' => floatval(Arr::get($orderItem, 'unit_price')) * intval(Arr::get($orderItem, 'quantity')),
509 'unit_price' => floatval(Arr::get($orderItem, 'unit_price', 0)),
510 'recurring_tax_total' => floatval(Arr::get($orderItem, 'tax_amount')),
511 'recurring_total' => floatval(Arr::get($orderItem, 'unit_price')),
512 'line_total' => floatval(Arr::get($orderItem, 'line_total')),
513 'trial_days' => empty(Arr::get($otherInfo, 'trial_days')) ? 0 : Arr::get($otherInfo, 'trial_days'),
514 'item_name' => Arr::get($orderItem, 'title') . ' ' . Arr::get($orderItem, 'post_title'),
515 'title' => Arr::get($orderItem, 'title') . ' ' . Arr::get($orderItem, 'post_title'),
516 'quantity' => intval(Arr::get($orderItem, 'quantity')),
517 'variation_id' => Arr::get($orderItem, 'object_id', 0),
518 'id' => Arr::get($orderItem, 'id'),
519 'other_info' => Arr::get($orderItem, 'other_info'),
520 ];
521 }
522
523 private static function prepareRegularItem(array $orderItem): array
524 {
525 return [
526 'product_id' => Arr::get($orderItem, 'post_id'),
527 'object_id' => Arr::get($orderItem, 'object_id', ''),
528 'payment_type' => Arr::get($orderItem, 'payment_type'),
529 'variation_id' => Arr::get($orderItem, 'id'),
530 'quantity' => intval(Arr::get($orderItem, 'quantity')),
531 'post_title' => Arr::get($orderItem, 'post_title'),
532 'title' => Arr::get($orderItem, 'title'),
533 'unit_price' => floatval(Arr::get($orderItem, 'unit_price', 0)),
534 'item_price' => floatval(Arr::get($orderItem, 'unit_price')),
535 'line_total' => floatval(Arr::get($orderItem, 'line_total')),
536 'fallback_title' => Arr::get($orderItem, 'title'),
537 ];
538 }
539
540 /**
541 * Get the root order ID from a given order.
542 *
543 * @param Order $order
544 * @return id|null
545 */
546 public static function getRootOrderId($order, $visited = [])
547 {
548 if (in_array($order->id, $visited)) {
549 return null; // breaking the loop
550 }
551
552 $visited[] = $order->id;
553 if (empty($order->parent_id)) {
554 return $order->id;
555 }
556
557 $parentOrder = Order::query()->where('id', $order->parent_id)->first();
558
559 if (!$parentOrder) {
560 return $order->id;
561 }
562
563 return static::getRootOrderId($parentOrder, $visited);
564 }
565
566 public static function getCouponDiscountTotal($orderItems)
567 {
568 if (empty($orderItems)) {
569 return 0;
570 }
571
572 $coupon_discount_total = 0;
573 foreach ($orderItems as $item) {
574 $coupon_discount_total += Arr::get($item, 'discount_total', 0);
575 }
576
577 return $coupon_discount_total;
578 }
579
580 public static function getNextReceiptNumber()
581 {
582 $lastOrder = Order::query()->max('receipt_number');
583 if (empty($lastOrder)) {
584 $lastOrder = 0;
585 }
586
587 $nextOrderNumber = $lastOrder + 1;
588
589 $min_receipt_number = (new StoreSettings())->get('min_receipt_number') ?? 1;
590 $minReceiptNumber = apply_filters('fluent_cart/min_receipt_number', $min_receipt_number);
591
592 if ($nextOrderNumber < $minReceiptNumber) {
593 $nextOrderNumber = $minReceiptNumber;
594 }
595
596 return $nextOrderNumber;
597 }
598
599 public static function getInvoicePrefix()
600 {
601 $prefix = (new StoreSettings())->get('inv_prefix') ?? 'INV-';
602 return apply_filters('fluent_cart/invoice_prefix', $prefix);
603 }
604
605
606 public static function transformSubscription(Subscription $subscription)
607 {
608 return [
609 'uuid' => $subscription->uuid,
610 'vendor_subscription_id' => $subscription->vendor_subscription_id,
611 'status' => $subscription->status,
612 'overridden_status' => $subscription->overridden_status,
613 'next_billing_date' => $subscription->next_billing_date,
614 'billing_info' => $subscription->billingInfo,
615 'current_payment_method' => $subscription->current_payment_method,
616 'payment_method' => $subscription->payment_method,
617 'payment_info' => $subscription->payment_info,
618 'bill_times' => $subscription->bill_times,
619 'bill_count' => $subscription->bill_count,
620 'config' => $subscription->config,
621 'reactivate_url' => $subscription->getReactivateUrl(),
622 'can_upgrade' => $subscription->canUpgrade(),
623 'can_switch_payment_method' => $subscription->canSwitchPaymentMethod(),
624 'can_update_payment_method' => $subscription->canUpdatePaymentMethod(),
625 'collection_method' => $subscription->collection_method,
626 'is_auto_charged' => $subscription->isSystem(),
627 'item_name' => $subscription->display_item_name
628 ];
629 }
630
631 /**
632 * A system renewal order still inside its auto-charge retry window has no
633 * Pay Now — the charge is coming automatically. Other renewals unaffected.
634 */
635 protected static function renewalPayNowAllowed(Order $order): bool
636 {
637 if (!$order->parent_id) {
638 return true;
639 }
640
641 $subscription = Subscription::query()->where('parent_order_id', $order->parent_id)->first();
642
643 if (!$subscription || !$subscription->isSystem()) {
644 return true;
645 }
646
647 return SystemChargeService::isExhausted($subscription, $order);
648 }
649
650 public static function transformTransaction(OrderTransaction $transaction)
651 {
652 $data = [
653 'uuid' => $transaction->uuid,
654 'invoice_no' => $transaction->order ? $transaction->order->invoice_no : 'n/a',
655 'created_at' => $transaction->created_at->format('Y-m-d H:i:s'),
656 'total' => $transaction->total,
657 'order_type' => $transaction->order_type,
658 'currency' => $transaction->currency,
659 'status' => $transaction->status,
660 'payment_method' => $transaction->payment_method,
661 'card_brand' => $transaction->card_brand,
662 'card_last_4' => $transaction->card_last_4,
663 'vendor_charge_id' => $transaction->vendor_charge_id,
664 'transaction_type' => $transaction->transaction_type,
665 'receipt_download_url' => $transaction->order ? $transaction->order->getReceiptDownloadUrl() : '',
666 ];
667
668 if ($transaction->order && self::canGenerateReceiptPdf()) {
669 $data['receipt_view_url'] = $transaction->order->getReceiptViewUrl();
670 }
671
672 if ($transaction->order
673 && $transaction->order->type === Status::ORDER_TYPE_RENEWAL
674 && $transaction->status === Status::TRANSACTION_PENDING
675 && self::renewalPayNowAllowed($transaction->order)
676 ) {
677 $data['custom_checkout_url'] = add_query_arg([
678 'fluent-cart' => 'custom_checkout',
679 'order_hash' => $transaction->order->uuid,
680 ], home_url('/'));
681 }
682
683 return $data;
684 }
685
686 public static function canGenerateReceiptPdf(): bool
687 {
688 return (bool) apply_filters(
689 'fluent_cart/pdf/can_generate_receipt',
690 App::isProActive() && defined('FLUENT_PDF')
691 );
692 }
693
694 }
695