PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.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 All 51 releases
fluent-cart / app / Services / OrderService.php

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

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