PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.2
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.2
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 / Helpers / CheckoutProcessor.php

CheckoutProcessor.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.2, at app/Helpers/CheckoutProcessor.php

1,214 lines 51.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\Helpers;
4
5 use FluentCart\Api\Checkout\CheckoutApi;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\Models\Cart;
8 use FluentCart\App\Models\Coupon;
9 use FluentCart\App\Models\Order;
10 use FluentCart\App\Models\OrderItem;
11 use FluentCart\App\Models\Product;
12 use FluentCart\App\Models\ProductVariation;
13 use FluentCart\App\Models\Subscription;
14 use FluentCart\App\Modules\Tax\TaxCalculator;
15 use FluentCart\Framework\Support\Arr;
16 use FluentCart\App\Helpers\Helper;
17
18 class CheckoutProcessor
19 {
20
21 // Raw Data
22 private $cartItems = [];
23 private $args = [];
24
25 // Order Related Data
26 private $formattedIOrderItems = [];
27 private $orderData = [];
28 private $subscriptionData = [];
29
30 // Models
31
32 private $orderModel;
33
34 private $transactionModel;
35
36 private $subscriptionModel;
37
38 // Fee tracking
39 private $feeTotal = 0;
40
41 // Store Settings
42 private $storeSettings;
43
44 private $couponDiscountTotal = 0;
45
46 private $manualDiscountTotal = 0;
47
48 private $prorateCreditTotal = 0;
49
50 private $upgradeDiscountTotal = 0;
51
52 public function __construct($cartItems = [], $args = [])
53 {
54 $this->storeSettings = new StoreSettings();
55 $this->cartItems = $cartItems;
56 $this->args = $args;
57
58 $this->prepareData();
59 }
60
61 private function prepareData()
62 {
63 $this->prepareOrderItems();
64 $this->prepareOrderData();
65 $this->prepareSubscriptionData();
66 }
67
68 public function createDraftOrder($prevOrder = null)
69 {
70 if ($prevOrder) {
71 return $this->getAdjustedOrder($prevOrder);
72 }
73
74 $customerId = Arr::get($this->args, 'customer_id', '');
75 if (!$customerId) {
76 return new \WP_Error('customer_id_missing', __('Customer ID is required to create a draft order.', 'fluent-cart'));
77 }
78
79 $orderData = $this->orderData;
80 $orderData['customer_id'] = $customerId;
81 if (empty($orderData['currency'])) {
82 $orderData['currency'] = $this->storeSettings->getCurrency();
83 }
84
85 if (empty($orderData['mode'])) {
86 $orderData['mode'] = $this->storeSettings->get('order_mode', 'test');
87 }
88
89 if (empty($orderData['fee_total'])) {
90 unset($orderData['fee_total']);
91 }
92
93 $this->orderModel = \FluentCart\App\Models\Order::query()->create($orderData);
94
95 if (!$this->orderModel) {
96 return new \WP_Error('order_creation_failed', __('Failed to create order.', 'fluent-cart'));
97 }
98
99 // save order meta
100 if (Arr::get($this->args, 'tax_id', 0)) {
101 $this->orderModel->updateMeta('tax_id', Arr::get($this->args, 'tax_id', 0));
102 }
103
104 // Store tax meta for mixed carts (tax_behavior=3) - used by payment gateways and renewals
105 $this->persistTaxMeta();
106
107 // Let's create the order items
108 $normalOrderItems = array_filter($this->formattedIOrderItems, function ($item) {
109 return $item['payment_type'] != 'signup_fee';
110 });
111
112 foreach ($normalOrderItems as $orderItem) {
113 $orderItem['order_id'] = $this->orderModel->id;
114 $orderItem['line_total'] = $orderItem['subtotal'] - $orderItem['discount_total'];
115 $additionalItems = [];
116 $bundleItems = [];
117 if ($orderItem['payment_type'] == 'subscription') {
118 // this is a subscription type. We may have additional_items
119 $additionalItems = Arr::get($orderItem, 'additional_items', []);
120 unset($orderItem['additional_items']);
121 }
122
123 if (Arr::get($orderItem, 'other_info.is_bundle_product', 'no') == 'yes') {
124 $bundleItems = Arr::get($orderItem, 'bundle_items', []);
125 unset($orderItem['bundle_items']);
126 }
127
128 if (!empty($orderItem['coupon_discount'])) {
129 $lineMeta = Arr::get($orderItem, 'line_meta', []);
130 $lineMeta['coupon_discount'] = (int) $orderItem['coupon_discount'];
131 $orderItem['line_meta'] = $lineMeta;
132 }
133
134 $createdItem = OrderItem::query()->create($orderItem);
135
136 if ($additionalItems) {
137 $additionalItemIds = [];
138 foreach ($additionalItems as $additionalItem) {
139 $additionalItem['order_id'] = $this->orderModel->id;
140 $additionalItem['line_total'] = Arr::get($additionalItem, 'subtotal', 0) - Arr::get($additionalItem, 'discount_total', 0);
141 $mata = Arr::get($additionalItem, 'line_meta', []);
142 $mata['parent_item_id'] = $createdItem->id;
143 if (!empty($additionalItem['coupon_discount'])) {
144 $mata['coupon_discount'] = (int) $additionalItem['coupon_discount'];
145 }
146 $additionalItem['line_meta'] = $mata;
147 $childItem = OrderItem::query()->create($additionalItem);
148 $additionalItemIds[] = $childItem->id;
149 }
150
151 $createdItem->fill([
152 'line_meta' => array_merge(
153 $createdItem->line_meta,
154 [
155 'additional_item_ids' => $additionalItemIds
156 ]
157 )
158 ])->save();
159 }
160
161 if ($bundleItems) {
162 $bundleItemIds = [];
163 foreach ($bundleItems as $bundleItem) {
164 $bundleItem['order_id'] = $this->orderModel->id;
165 $bundleItem['line_total'] = Arr::get($bundleItem, 'subtotal', 0) - Arr::get($bundleItem, 'discount_total', 0);
166 $bundleItem['payment_type'] = 'bundle';
167 $meta = Arr::get($bundleItem, 'line_meta', []);
168 $meta['bundle_parent_item_id'] = $createdItem->id;
169 if (!empty($bundleItem['coupon_discount'])) {
170 $meta['coupon_discount'] = (int) $bundleItem['coupon_discount'];
171 }
172 $bundleItem['line_meta'] = $meta;
173 $bundleItem = OrderItem::query()->create($bundleItem);
174 $bundleItemIds[] = $bundleItem->id;
175 }
176
177 $createdItem->fill([
178 'line_meta' => array_merge(
179 $createdItem->line_meta,
180 [
181 'bundle_item_ids' => $bundleItemIds
182 ]
183 )
184 ])->save();
185 }
186 }
187
188
189 // Let's create the subscription if exists
190 /*
191 * TODO : on renewal order we shouldn't create another subscription,
192 * basically on manual renew we just create a new sub on gateway and update the existing one on our database
193 * which automates the renewal cycle for the existing subscription
194 * ....created new sub remains on pending state, will create inconsistency
195 * */
196
197 if ($this->subscriptionData) {
198 $subscriptionData = $this->subscriptionData;
199 $subscriptionData['customer_id'] = $customerId;
200 $subscriptionData['parent_order_id'] = $this->orderModel->id;
201
202 $this->subscriptionModel = Subscription::query()->create($subscriptionData);
203 }
204
205 // Let's create the transaction
206 $transactionData = [
207 'order_id' => $this->orderModel->id,
208 'order_type' => $this->orderModel->type,
209 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
210 'subscription_id' => $this->subscriptionModel ? $this->subscriptionModel->id : NULL,
211 'payment_method' => $this->orderModel->payment_method,
212 'payment_mode' => $this->orderModel->mode,
213 'payment_method_type' => '',
214 'status' => Status::PAYMENT_PENDING,
215 'currency' => $this->orderModel->currency,
216 'total' => $this->orderModel->total_amount,
217 'rate' => 1,
218 'meta' => [],
219 ];
220
221 $this->transactionModel = \FluentCart\App\Models\OrderTransaction::query()->create($transactionData);
222
223 // insert the applied coupons
224 $this->insertAppliedCoupons(
225 Arr::get($this->args, 'applied_coupons', []),
226 false,
227 $this->orderModel
228 );
229
230 $cartHash = Arr::get($this->args, 'cart_hash', '');
231 if ($cartHash) {
232 $cart = Cart::query()->where('cart_hash', $cartHash)->first();
233 if ($cart) {
234 $cart->order_id = $this->orderModel->id;
235 $cart->customer_id = $this->orderModel->customer_id;
236 $cart->stage = 'intended';
237 $customer = $this->orderModel->customer;
238
239 if ($customer) {
240 $cart->first_name = $customer->first_name;
241 $cart->last_name = $customer->last_name;
242 $cart->email = $customer->email;
243 $cart->user_id = $customer->user_id;
244 }
245
246 $cart->save();
247 $actions = Arr::get($cart->checkout_data, '__after_draft_created_actions__', []);
248 if ($actions) {
249 foreach ($actions as $actionName) {
250 $actionName = (string)$actionName;
251 if (has_action($actionName)) {
252 do_action($actionName, [
253 'order' => $this->orderModel,
254 'cart' => $cart,
255 ]);
256 }
257 }
258
259 // We are just renewing it!
260 $this->orderModel = \FluentCart\App\Models\Order::query()
261 ->where('id', $this->orderModel->id)
262 ->first();
263 }
264 }
265 }
266
267 // We are almost done!
268 return $this->orderModel;
269 }
270
271 private function getAdjustedOrder(Order $prevOrder)
272 {
273 $isLocked = Arr::get($this->args, 'is_locked', false);
274
275 $orderData = $this->orderData;
276 $customerId = Arr::get($this->args, 'customer_id', '');
277 if ($customerId) {
278 $orderData['customer_id'] = $customerId;
279 }
280
281 if ($isLocked) {
282 $orderData = array_filter(Arr::only($orderData, ['note', 'payment_method', 'ip_address', 'customer_id', 'shipping_total', 'total_amount', 'tax_total', 'fee_total', 'shipping_tax']), function ($value) {
283 return $value !== null && $value !== '';
284 });
285
286 $prevConfig = $prevOrder->config;
287 $prevConfig['user_tz'] = Arr::get($this->args, 'user_tz', '');
288 $orderData['config'] = $prevConfig;
289 $prevOrder->fill($orderData);
290 $prevOrder->save();
291 } else {
292 $prevOrder->fill($orderData);
293 $prevOrder->save();
294 }
295
296 $this->orderModel = $prevOrder;
297
298 if (!$this->orderModel) {
299 return new \WP_Error('order_creation_failed', __('Failed to create order.', 'fluent-cart'));
300 }
301
302 //for previous order if tax is enabled then we need to update the tax total
303 $taxSettings = get_option('fluent_cart_tax_configuration_settings', []);
304 $taxEnabled = Arr::get($taxSettings, 'enable_tax', 'no');
305
306 if ($isLocked && $taxEnabled !== 'yes') {
307 // Locked orders skip full item sync, but fee items must stay in sync with fee_total
308 $this->syncFeeItems();
309 }
310
311 if (!$isLocked || $taxEnabled === 'yes') {
312 // Let's create the order items
313 $normalOrderItems = array_filter($this->formattedIOrderItems, function ($item) {
314 return $item['payment_type'] != 'signup_fee';
315 });
316
317 $createdItemIds = [];
318 $taxTotal = 0;
319 foreach ($normalOrderItems as $orderItem) {
320 $orderItem['order_id'] = $this->orderModel->id;
321 $orderItem['quantity'] = max(1, (int)$orderItem['quantity']);
322 $orderItem['line_total'] = $orderItem['subtotal'] - $orderItem['discount_total'];
323 $taxTotal += $orderItem['tax_amount'];
324 $additionalItems = [];
325 $bundleItems = [];
326 if ($orderItem['payment_type'] == 'subscription') {
327 // this is a subscription type. We may have additional_items
328 $additionalItems = Arr::get($orderItem, 'additional_items', []);
329 unset($orderItem['additional_items']);
330 }
331
332 if (Arr::get($orderItem, 'other_info.is_bundle_product', 'no') == 'yes') {
333 $bundleItems = Arr::get($orderItem, 'bundle_items', []);
334 unset($orderItem['bundle_items']);
335 }
336
337 $existingItem = OrderItem::query()->where('order_id', $this->orderModel->id)
338 ->whereNotIn('id', $createdItemIds)
339 ->first();
340
341 if ($existingItem) {
342 $existingItem->fill($orderItem);
343 $existingItem->save();
344 $createdItem = $existingItem;
345 } else {
346 $createdItem = OrderItem::query()->create($orderItem);
347 }
348
349 $createdItemIds[] = $createdItem->id;
350
351 if ($additionalItems) {
352 $additionalItemIds = [];
353 foreach ($additionalItems as $additionalItem) {
354 $additionalItem['order_id'] = $this->orderModel->id;
355 $additionalItem['quantity'] = max(1, (int)$additionalItem['quantity']);
356 $additionalItem['line_total'] = $additionalItem['subtotal'] - $additionalItem['discount_total'];
357 $mata = Arr::get($additionalItem, 'line_meta', []);
358 $mata['parent_item_id'] = $createdItem->id;
359 $additionalItem['line_meta'] = $mata;
360 $childItem = OrderItem::query()->create($additionalItem);
361 $createdItemIds[] = $childItem->id;
362 $additionalItemIds[] = $childItem->id;
363
364 }
365
366 $createdItem->fill([
367 'line_meta' => array_merge(
368 $createdItem->line_meta,
369 [
370 'additional_item_ids' => $additionalItemIds
371 ]
372 )
373 ])->save();
374 }
375
376 if ($bundleItems) {
377 $bundleItemIds = [];
378 foreach ($bundleItems as $bundleItem) {
379 $bundleItem['order_id'] = $this->orderModel->id;
380 $bundleItem['quantity'] = Arr::get($orderItem, 'quantity', 1);
381 $bundleItem['line_total'] = Arr::get($bundleItem, 'subtotal', 0) - Arr::get($bundleItem, 'discount_total', 0);
382 $bundleItem['payment_type'] = 'bundle';
383 $meta['bundle_parent_item_id'] = $createdItem->id;
384 $bundleItem['line_meta'] = $meta;
385 $childItem = OrderItem::query()->create($bundleItem);
386 $createdItemIds[] = $childItem->id;
387 $bundleItemIds[] = $childItem->id;
388 }
389
390 $createdItem->fill([
391 'line_meta' => array_merge(
392 $createdItem->line_meta,
393 [
394 'bundle_item_ids' => $bundleItemIds
395 ]
396 )
397 ])->save();
398 }
399 }
400 if ($taxTotal && !$this->orderModel->tax_total && !$isLocked) {
401 $this->orderModel->tax_total = $taxTotal;
402 $this->orderModel->total_amount += $taxTotal;
403 $this->orderModel->save();
404 }
405
406 OrderItem::query()->where('order_id', $this->orderModel->id)->whereNotIn('id', $createdItemIds)->delete();
407
408 // Let's create the subscription if exists
409 if ($this->subscriptionData) {
410 if ($this->orderModel->type === Status::ORDER_TYPE_RENEWAL) {
411 // it's a renewal order
412 $this->subscriptionModel = Subscription::query()
413 ->where('parent_order_id', $this->orderModel->parent_id)
414 ->first();
415 } else {
416 $subscriptionData = $this->subscriptionData;
417 $subscriptionData['customer_id'] = $customerId;
418 $subscriptionData['parent_order_id'] = $this->orderModel->id;
419 $existingSubscription = Subscription::query()->where('parent_order_id', $this->orderModel->id)->first();
420 if ($existingSubscription) {
421 $existingSubscription->fill($subscriptionData);
422 $existingSubscription->save();
423 $this->subscriptionModel = $existingSubscription;
424 } else {
425 $this->subscriptionModel = Subscription::query()->create($subscriptionData);
426 }
427 }
428 } else {
429 Subscription::query()->where('parent_order_id', $this->orderModel->id)->delete();
430 }
431 }
432
433 // Reload the order to get fresh item data after updates
434 $this->orderModel = $this->orderModel->fresh();
435
436 $this->persistTaxMeta();
437
438 // Let's create the transaction
439 $transactionData = [
440 'order_id' => $this->orderModel->id,
441 'order_type' => $this->orderModel->type,
442 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
443 'subscription_id' => $this->subscriptionModel ? $this->subscriptionModel->id : NULL,
444 'payment_method' => $this->orderModel->payment_method,
445 'payment_mode' => $this->orderModel->mode,
446 'payment_method_type' => '',
447 'status' => Status::PAYMENT_PENDING,
448 'currency' => $this->orderModel->currency,
449 'total' => $this->orderModel->total_amount,
450 'rate' => 1,
451 'meta' => [],
452 ];
453
454 if ($isLocked && $prevOrder->parent_id) {
455 $prevSubscription = Subscription::query()
456 ->where('parent_order_id', $prevOrder->parent_id)
457 ->first();
458
459 if ($prevSubscription) {
460 $transactionData['subscription_id'] = $prevSubscription->id;
461 }
462 }
463
464 $existingTransaction = \FluentCart\App\Models\OrderTransaction::query()
465 ->where('order_id', $this->orderModel->id)
466 ->first();
467
468 if ($existingTransaction) {
469 $existingTransaction->fill($transactionData);
470 $existingTransaction->save();
471 $this->transactionModel = $existingTransaction;
472 } else {
473 $this->transactionModel = \FluentCart\App\Models\OrderTransaction::query()->create($transactionData);
474 }
475
476 // For locked carts (e.g. custom checkout), preserve the original order's applied coupon records.
477 // Re-inserting would delete existing records and incorrectly increment the coupon use_count.
478 if (!$isLocked) {
479 $this->insertAppliedCoupons(Arr::get($this->args, 'applied_coupons', []), true, $prevOrder);
480 }
481
482 $cartHash = Arr::get($this->args, 'cart_hash', '');
483
484 if ($cartHash) {
485 $cart = Cart::query()->where('cart_hash', $cartHash)->first();
486 if ($cart) {
487 $cart->order_id = $this->orderModel->id;
488 $cart->customer_id = $this->orderModel->customer_id;
489 $cart->stage = 'intended';
490 $cart->ip_address = $this->orderModel->ip_address;
491 $cart->save();
492 }
493 }
494
495 // We are almost done!
496 return $this->orderModel;
497 }
498
499 private function persistTaxMeta()
500 {
501 $exclusiveTaxTotal = (int) Arr::get($this->args, 'exclusive_tax_total', 0);
502 $storeTaxBehavior = (int) Arr::get($this->args, 'store_tax_behavior', 0);
503 $feeTax = (int) Arr::get($this->args, 'fee_tax', 0);
504 $feeTaxLines = (array) Arr::get($this->args, 'fee_tax_lines', []);
505
506 $this->orderModel->updateMeta('exclusive_tax_total', $exclusiveTaxTotal);
507 $this->orderModel->updateMeta('store_tax_behavior', $storeTaxBehavior);
508 $this->orderModel->updateMeta('fee_tax', $feeTax);
509
510 if (!empty($feeTaxLines)) {
511 $this->orderModel->updateMeta('fee_tax_lines', $feeTaxLines);
512 } else {
513 $this->orderModel->deleteMeta('fee_tax_lines');
514 }
515 }
516
517 private function insertAppliedCoupons($appliedCoupons, $removeOlds = false, $order = null): void
518 {
519 if ($removeOlds) {
520 $this->orderModel->appliedCoupons()->delete();
521 }
522
523 $couponCodes = Arr::pluck($appliedCoupons, 'code');
524
525 $customerId = $this->orderModel->customer_id;
526 if ($order instanceof Order) {
527 $customerId = $order->customer_id;
528 }
529
530 if (!empty($couponCodes)) {
531 $coupons = Coupon::query()->whereIn('code', $couponCodes)->get()
532 ->keyBy('code')
533 ->toArray();
534
535 foreach ($coupons as $code => &$coupon) {
536 $coupon['coupon_id'] = $appliedCoupons[$code]['id'];
537 $coupon['amount'] = $appliedCoupons[$code]['discount'];
538 $coupon['customer_id'] = $customerId;
539 }
540 $this->orderModel->appliedCoupons()->createMany($coupons);
541
542 Coupon::query()
543 ->whereIn('code', $couponCodes)
544 ->increment('use_count', 1);
545 }
546 }
547
548 public function getTransaction()
549 {
550 return $this->transactionModel;
551 }
552
553 public function getOrder()
554 {
555 return $this->orderModel;
556 }
557
558 public function getSubscription()
559 {
560 return $this->subscriptionModel;
561 }
562
563 private function prepareOrderItems()
564 {
565 $formattedItems = [];
566
567 foreach ($this->cartItems as $cartItem) {
568 $unitPrice = (int)Arr::get($cartItem, 'unit_price', 0);
569 $quantity = (int)Arr::get($cartItem, 'quantity', 1);
570
571 $this->couponDiscountTotal += (int)Arr::get($cartItem, 'coupon_discount', 0);
572 $this->manualDiscountTotal += (int)Arr::get($cartItem, 'manual_discount', 0);
573
574 $discountTotal = (int)Arr::get($cartItem, 'manual_discount', 0) + (int)Arr::get($cartItem, 'coupon_discount', 0);
575 $shippingCharge = (int)Arr::get($cartItem, 'shipping_charge', 0);
576
577 $subtotal = (int) Arr::get($cartItem, 'subtotal', $unitPrice * $quantity);
578 $args = Arr::get($cartItem, 'other_info', []);
579 $paymentType = Arr::get($args, 'payment_type', 'default');
580
581 $postTitle = Arr::get($cartItem, 'product_title', '');
582 $variationTitle = Arr::get($cartItem, 'variation_title', '');
583
584 if (!$postTitle) {
585 if (Arr::get($cartItem, 'is_custom', false)) {
586 $postTitle = Arr::get($cartItem, 'post_title', '');
587 } else {
588 $product = Product::query()->find(Arr::get($cartItem, 'post_id', 0));
589 if ($product) {
590 $postTitle = $product->post_title;
591 }
592 }
593 }
594
595 if (!$variationTitle) {
596 if (Arr::get($cartItem, 'is_custom', false)) {
597 $variationTitle = Arr::get($cartItem, 'title', '');
598 } else {
599 $variation = ProductVariation::query()->find(Arr::get($cartItem, 'object_id', 0));
600 if ($variation) {
601 $variationTitle = $variation->variation_title;
602 }
603 }
604 }
605
606 // Snapshot package dimensions into other_info for email/PDF rendering
607 if (Arr::get($cartItem, 'fulfillment_type') === 'physical') {
608 $packageSlug = Arr::get($args, 'package_slug', '');
609 $package = Helper::getPackageBySlug($packageSlug);
610 if ($package) {
611 $args['package_name'] = Arr::get($package, 'name', '');
612 $args['package_type'] = Arr::get($package, 'type', '');
613 $args['package_length'] = Arr::get($package, 'length', '');
614 $args['package_width'] = Arr::get($package, 'width', '');
615 $args['package_height'] = Arr::get($package, 'height', '');
616 $args['package_dimension_unit'] = Arr::get($package, 'dimension_unit', 'cm');
617 $args['package_weight'] = Arr::get($package, 'weight', 0);
618 $args['package_weight_unit'] = Arr::get($package, 'weight_unit', 'kg');
619 }
620 }
621
622 $item = [
623 'payment_type' => $paymentType,
624 'post_id' => Arr::get($cartItem, 'post_id'),
625 'object_id' => Arr::get($cartItem, 'object_id'),
626 'post_title' => $postTitle,
627 'title' => $variationTitle,
628 'fulfillment_type' => Arr::get($cartItem, 'fulfillment_type', 'digital'),
629 'quantity' => $quantity,
630 'cost' => (int)Arr::get($cartItem, 'cost', 0),
631 'unit_price' => $unitPrice,
632 'subtotal' => $subtotal,
633 'tax_amount' => (int)Arr::get($cartItem, 'tax_amount', 0),
634 'shipping_charge' => $shippingCharge,
635 'discount_total' => $discountTotal,
636 'coupon_discount' => (int)Arr::get($cartItem, 'coupon_discount', 0),
637 'other_info' => $args,
638 'line_meta' => Arr::get($cartItem, 'line_meta', []),
639 ];
640
641 if (isset($cartItem['recurring_discounts'])) {
642 $item['recurring_discounts'] = $cartItem['recurring_discounts'];
643 }
644
645 $childItem = null;
646 if ($paymentType === 'subscription' && Arr::get($cartItem, 'other_info.signup_fee', 0)) {
647 // We have a signup fee for subscription
648 $signupFeeAmount = (int)Arr::get($cartItem, 'other_info.signup_fee', 0);
649
650 $signupFeeTax = (int)Arr::get($cartItem, 'other_info.signup_fee_tax', 0);
651
652 // Nest under tax_config — the same shape regular items and the admin
653 // order path use. Readers keep a fallback for the legacy flat shape.
654 $signupFeeTaxConfig = Arr::get($cartItem, 'signup_fee_tax_config', []);
655
656 $childDiscountTotal = 0;
657 $childCouponDiscount = 0;
658 $signupFeeSubtotal = $signupFeeAmount * $quantity;
659 $couponDiscount = $item['coupon_discount'];
660
661 $hasTrialDays = Arr::get($cartItem, 'other_info.trial_days', 0) > 0;
662
663 if ($discountTotal && !$hasTrialDays) {
664 $childDiscountTotal = (float)($discountTotal / ($subtotal + $signupFeeSubtotal) * $signupFeeSubtotal);
665 $discountTotal -= $childDiscountTotal;
666 $childCouponDiscount = (int) round($couponDiscount * $signupFeeSubtotal / ($subtotal + $signupFeeSubtotal));
667 $couponDiscount -= $childCouponDiscount;
668 } elseif ($discountTotal && $hasTrialDays) { // if trial days , then discount should be applied on signup fee only
669 $childDiscountTotal = min($discountTotal, $signupFeeSubtotal);
670 $discountTotal = 0;
671 $childCouponDiscount = min($couponDiscount, $signupFeeSubtotal);
672 $couponDiscount = 0;
673 }
674
675 $childItem = [
676 'payment_type' => 'signup_fee',
677 'post_id' => $item['post_id'],
678 'object_id' => $item['object_id'],
679 'post_title' => $item['post_title'],
680 'title' => Arr::get($cartItem, 'other_info.signup_fee_name', __('Signup Fee', 'fluent-cart')),
681 'fulfillment_type' => $item['fulfillment_type'],
682 'quantity' => $quantity,
683 'cost' => 0,
684 'unit_price' => $signupFeeAmount,
685 'subtotal' => $signupFeeSubtotal,
686 'tax_amount' => $signupFeeTax,
687 'shipping_charge' => 0,
688 'discount_total' => $childDiscountTotal,
689 'coupon_discount' => $childCouponDiscount,
690 'line_meta' => $signupFeeTaxConfig ? ['tax_config' => $signupFeeTaxConfig] : [],
691 ];
692
693 $item['discount_total'] = $discountTotal;
694 $item['coupon_discount'] = $couponDiscount;
695 $item['additional_items'] = [$childItem];
696
697 Arr::set($item, 'other_info.signup_fee', $signupFeeAmount);
698 Arr::set($item, 'other_info.signup_discount', $childDiscountTotal);
699 }
700
701 if (
702 Arr::get($cartItem, 'other_info.is_bundle_product', 'no') == 'yes'
703 || !empty(Arr::get($cartItem, 'other_info.bundle_child_ids', []))
704 ) {
705 $bundleItems = Arr::get($cartItem, 'child_variants', []);
706
707 foreach ($bundleItems as $bundleItem) {
708 $bundleChildItem = [
709 'payment_type' => 'bundle',
710 'post_id' => Arr::get($bundleItem, 'post_id', 0),
711 'object_id' => Arr::get($bundleItem, 'id', 0),
712 'post_title' => Arr::get($bundleItem, 'post_title', ''),
713 'title' => Arr::get($bundleItem, 'variation_title', ''),
714 'fulfillment_type' => Arr::get($bundleItem, 'fulfillment_type', 'digital'),
715 'quantity' => $quantity,
716 'cost' => 0,
717 'unit_price' => 0,
718 'subtotal' => 0,
719 'tax_amount' => 0,
720 'shipping_charge' => 0,
721 'discount_total' => 0,
722 'other_info' => [
723 'bundle_parent_product_id' => Arr::get($cartItem, 'post_id', 0),
724 'bundle_parent_variation_id' => Arr::get($cartItem, 'object_id', 0)
725 ],
726 ];
727
728 //TODO: if bundleItem price is included on for the bundle, then we need to set the price to the bundleItem price
729 // if (Arr::get($bundleItem, 'other_info.is_price_included', 'no') == 'yes') {
730 // $bundleChildItem['unit_price'] = Arr::get($bundleItem, 'unit_price', 0);
731 // $bundleChildItem['subtotal'] = Arr::get($bundleItem, 'subtotal', 0);
732 // $bundleChildItem['tax_amount'] = Arr::get($bundleItem, 'tax_amount', 0);
733 // $bundleChildItem['shipping_charge'] = Arr::get($bundleItem, 'shipping_charge', 0);
734 // $bundleChildItem['discount_total'] = Arr::get($bundleItem, 'discount_total', 0);
735 // }
736
737 $item['bundle_items'][] = $bundleChildItem;
738
739 }
740
741 }
742
743 $formattedItems[] = $item;
744 if ($childItem) {
745 $formattedItems[] = $childItem;
746 }
747 }
748
749 // Create order items for fees
750 $fees = (array)Arr::get($this->args, 'fees', []);
751 $this->feeTotal = 0;
752
753 foreach ($fees as $fee) {
754 $amount = (int)($fee['amount'] ?? 0);
755 if ($amount <= 0) {
756 continue;
757 }
758
759 $this->feeTotal += $amount;
760
761 $formattedItems[] = [
762 'payment_type' => 'fee',
763 'post_id' => 0,
764 'object_id' => 0,
765 'post_title' => '',
766 'title' => $fee['label'] ?? '',
767 'fulfillment_type' => 'digital',
768 'quantity' => 1,
769 'cost' => 0,
770 'unit_price' => $amount,
771 'subtotal' => $amount,
772 'tax_amount' => 0,
773 'shipping_charge' => 0,
774 'discount_total' => 0,
775 'other_info' => [
776 'payment_type' => 'fee',
777 'fee_key' => $fee['key'] ?? '',
778 'source' => $fee['source'] ?? 'custom',
779 'taxable' => !empty($fee['taxable']),
780 'meta' => $fee['meta'] ?? [],
781 ],
782 'line_meta' => [],
783 ];
784 }
785
786 $this->formattedIOrderItems = $formattedItems;
787 }
788
789 private function prepareSubscriptionData()
790 {
791 $subscriptionItems = array_filter($this->formattedIOrderItems, function ($item) {
792 return $item['payment_type'] === 'subscription';
793 });
794
795 $signupFeeItems = array_filter($this->formattedIOrderItems, function ($item) {
796 return $item['payment_type'] === 'signup_fee';
797 });
798
799 if (!$subscriptionItems) {
800 return;
801 }
802
803 if (count($subscriptionItems) > 1) {
804 return;
805 }
806
807 $item = reset($subscriptionItems);
808 $signupFeeItem = reset($signupFeeItems) ?? [];
809 $signupFeeTax = (int)Arr::get($signupFeeItem, 'tax_amount', 0);
810 $taxBehavior = Arr::get($this->args, 'tax_behavior', 0);
811
812 $recurringTotal = (int)$item['subtotal'];
813 $recurringTax = (int)Arr::get($item, 'other_info.recurring_tax', 0);
814
815 $recurringDiscountAmount = (int)Arr::get($item, 'recurring_discounts.amount', 0);
816
817 if ($recurringDiscountAmount && $recurringDiscountAmount > 0) {
818 $recurringTotal -= $recurringDiscountAmount;
819 }
820
821 // Add shipping charges to recurring total for physical subscription products
822 $shippingCharge = (int)Arr::get($this->args, 'shipping_charge', 0);
823 $isPhysicalProduct = Arr::get($item, 'fulfillment_type') === 'physical';
824 if ($isPhysicalProduct && $shippingCharge > 0) {
825 $recurringTotal += $shippingCharge;
826 }
827
828 $itemInclusive = (bool) Arr::get($item, 'line_meta.tax_config.inclusive', false);
829 if ($taxBehavior === 1 || ($taxBehavior === 3 && !$itemInclusive)) {
830 $recurringTotal += $recurringTax;
831 }
832
833 $signupFee = (int)Arr::get($signupFeeItem, 'subtotal', 0);
834
835 // in case of discount applied 'tax_amount' is different than recurring tax ,
836 $firstIterationTax = (int)Arr::get($item, 'tax_amount', 0) + $signupFeeTax;
837
838
839 // Calculate recurring amount including shipping for physical products
840 $recurringAmount = (int)$item['subtotal'];
841 if ($isPhysicalProduct && $shippingCharge > 0) {
842 $recurringAmount += $shippingCharge;
843 }
844
845 $discountTotal = $item['discount_total'] + Arr::get($signupFeeItem, 'discount_total', 0) + $this->prorateCreditTotal + $this->upgradeDiscountTotal;
846 $subscriptionPricing = $this->convertToSubscriptionFormat([
847 'initial_trial_days' => Arr::get($item, 'other_info.trial_days', 0),
848 'repeat_interval' => Arr::get($item, 'other_info.repeat_interval', 'monthly'),
849 'times' => Arr::get($item, 'other_info.times', 0),
850 'recurring_amount' => $recurringAmount,
851 'recurring_tax_total' => $recurringTax,
852 'recurring_total' => $recurringTotal,
853 'tax_behavior' => $taxBehavior,
854 'line_meta' => Arr::get($item, 'line_meta', []),
855 'signup_fee' => $signupFee,
856 'signup_fee_tax' => $signupFeeTax,
857 'first_iteration_tax' => $firstIterationTax,
858 'is_recurring_coupon' => Arr::get($item, 'is_recurring_coupon', 'no'),
859 'total_discount' => $discountTotal
860 ]);
861
862 // removable upon discussion
863 $subscriptionItem = [
864 'product_id' => $item['post_id'],
865 'current_payment_method' => Arr::get($this->orderData, 'payment_method'),
866 'object_id' => $item['object_id'],
867 'recurring_tax_total' => 0,
868 'recurring_total' => $recurringTotal, //use price not line_total to ignore discount
869 'item_name' => $item['post_title'] . ' - ' . $item['title'],
870 'bill_count' => 0,
871 'quantity' => 1,
872 'variation_id' => Arr::get($item, 'object_id', 0),
873 'status' => Status::SUBSCRIPTION_PENDING,
874 'config' => [
875 'is_trial_days_simulated' => Arr::get($subscriptionPricing, 'is_trial_days_simulated', 'no'),
876 'currency' => $this->orderData['currency']
877 ]
878 ];
879
880 // if recurring coupon is applied, we need to subtract the total discount from the recurring total
881 if (Arr::get($item, 'is_recurring_coupon', 'no') === 'yes') {
882 $subscriptionItem['recurring_total'] -= $discountTotal;
883 }
884
885 $this->subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
886 }
887
888 private function prepareOrderData()
889 {
890 $hasPhysical = array_filter($this->formattedIOrderItems, function ($item) {
891 return $item['fulfillment_type'] === 'physical';
892 });
893
894 $hasSubscription = array_filter($this->formattedIOrderItems, function ($item) {
895 return $item['payment_type'] === 'subscription';
896 });
897
898 $itemsSubtotal = array_reduce($this->formattedIOrderItems, function ($carry, $item) {
899 if (Arr::get($item, 'other_info.trial_days', 0) > 0) {
900 return $carry;
901 }
902 // Fee items are tracked separately via fee_total
903 if (Arr::get($item, 'payment_type') === 'fee') {
904 return $carry;
905 }
906 return $carry + $item['subtotal'];
907 }, 0);
908
909 $taxBehavior = (int) Arr::get($this->args, 'tax_behavior', 0);
910 $storeTaxBehavior = (int) Arr::get($this->args, 'store_tax_behavior', $taxBehavior);
911 $exclusiveTaxTotal = (int) Arr::get($this->args, 'exclusive_tax_total', 0);
912 $feeTax = (int) Arr::get($this->args, 'fee_tax', 0);
913
914 // Roll fee tax into fee_total for exclusive scenarios — gateways use fee_total as source of truth.
915 if ($feeTax && ($taxBehavior === 1 || ($taxBehavior === 3 && $storeTaxBehavior === 1))) {
916 $this->feeTotal += $feeTax;
917 }
918
919 $this->prorateCreditTotal = (int) Arr::get($this->args, 'prorate_credit', 0);
920 // Upgrade-path discount is a post-tax adjustment like the prorate credit: it does
921 // not reduce the taxable base (tax args were computed on the full price), it only
922 // reduces the payable total via manual_discount_total below.
923 $this->upgradeDiscountTotal = (int) Arr::get($this->args, 'upgrade_discount', 0);
924
925 $orderData = [
926 'status' => Status::ORDER_ON_HOLD,
927 'fulfillment_type' => $hasPhysical ? Status::FULFILLMENT_TYPE_PHYSICAL : Status::FULFILLMENT_TYPE_DIGITAL,
928 'type' => $hasSubscription ? Status::ORDER_TYPE_SUBSCRIPTION : Status::ORDER_TYPE_PAYMENT, // revisit this on manual renewal
929 'mode' => $this->storeSettings->get('order_mode', 'test'),
930 'shipping_status' => $hasPhysical ? 'unshipped' : '',
931 'customer_id' => '',
932 'payment_method' => Arr::get($this->args, 'payment_method', ''),
933 'payment_status' => Status::PAYMENT_PENDING,
934 'payment_method_title' => '',
935 'currency' => $this->storeSettings->get('currency'),
936 'subtotal' => $itemsSubtotal,
937 'discount_tax' => 0,
938 'manual_discount_total' => $this->manualDiscountTotal + $this->prorateCreditTotal + $this->upgradeDiscountTotal,
939 'coupon_discount_total' => $this->couponDiscountTotal,
940 'shipping_tax' => Arr::get($this->args, 'shipping_tax', 0),
941 'shipping_total' => Arr::get($this->args, 'shipping_charge', 0),
942 'fee_total' => $this->feeTotal,
943 'tax_total' => Arr::get($this->args, 'tax_total', 0),
944 'tax_behavior' => $taxBehavior,
945 // 'total_amount' => $this->orderTotals['total_amount'],
946 'total_paid' => 0,
947 'total_refund' => 0,
948 'rate' => 1,
949 'note' => Arr::get($this->args, 'note', ''),
950 'ip_address' => Arr::get($this->args, 'ip_address', ''),
951 'config' => [
952 'user_tz' => Arr::get($this->args, 'user_tz', ''),
953 'create_account_after_paid' => Arr::get($this->args, 'create_account_after_paid', 'no'),
954 'shipping_method_id' => Arr::get($this->args, 'shipping_method_id', 0),
955 'shipping_method_title' => Arr::get($this->args, 'shipping_method_title', ''),
956 'prorate_credit' => $this->prorateCreditTotal,
957 'upgrade_discount' => $this->upgradeDiscountTotal,
958 ],
959 ];
960
961 if ($taxBehavior === 1) {
962 // Pure exclusive: fee_tax is already in fee_total; exclude it here to avoid double-count.
963 $estimatedTaxTotal = $orderData['tax_total'] - $feeTax;
964 $estimatedShippingTax = $orderData['shipping_tax'];
965 } elseif ($taxBehavior === 3) {
966 // Mixed: fee_tax rolled into fee_total for exclusive store; shipping still additive.
967 $estimatedTaxTotal = $exclusiveTaxTotal;
968 $estimatedShippingTax = ($storeTaxBehavior === 1) ? $orderData['shipping_tax'] : 0;
969 } else {
970 // Inclusive (2) or reverse-charge (0): nothing to add to total; tax is in item prices.
971 $estimatedTaxTotal = 0;
972 $estimatedShippingTax = 0;
973 if ($taxBehavior !== 2) {
974 // Reverse-charge (0): zero stored columns — no tax applies.
975 // Inclusive (2): keep orderData values so reporting surfaces can read them.
976 $orderData['tax_total'] = 0;
977 $orderData['shipping_tax'] = 0;
978 }
979 }
980
981 $totalAmount = $orderData['subtotal']
982 - $orderData['coupon_discount_total']
983 - $orderData['manual_discount_total']
984 + $orderData['fee_total']
985 + $orderData['shipping_total']
986 + $estimatedTaxTotal
987 + $estimatedShippingTax;
988
989 $orderData['total_amount'] = $totalAmount > 0 ? $totalAmount : 0;
990 $this->orderData = $orderData;
991 }
992
993 private function syncFeeItems()
994 {
995 $orderId = $this->orderModel->id;
996
997 // Remove existing fee items
998 OrderItem::query()->where('order_id', $orderId)->where('payment_type', 'fee')->delete();
999
1000 // Create new fee items from formatted items
1001 $feeItems = array_filter($this->formattedIOrderItems, function ($item) {
1002 return $item['payment_type'] === 'fee';
1003 });
1004
1005 foreach ($feeItems as $feeItem) {
1006 $feeItem['order_id'] = $orderId;
1007 $feeItem['quantity'] = 1;
1008 $feeItem['line_total'] = $feeItem['subtotal'] - $feeItem['discount_total'];
1009 OrderItem::query()->create($feeItem);
1010 }
1011 }
1012
1013 /**
1014 * @param $inputData
1015 * @return array
1016 */
1017 private function convertToSubscriptionFormat($inputData)
1018 {
1019
1020 /**
1021 * Normal Subscription $100/month
1022 * {
1023 * 'trial_days' => 0,
1024 * 'repeat_interval' => 'month',
1025 * 'times' => 0, // 0 means unlimited
1026 * 'recurring_amount' => 100,
1027 * 'signup_fee' => 0
1028 * }
1029 *
1030 * 30 Days Trial $100/month
1031 * {
1032 * 'trial_days' => 30,
1033 * 'repeat_interval' => 'month',
1034 * 'times' => 0, // 0 means unlimited
1035 * 'recurring_amount' => 100,
1036 * }
1037 *
1038 * $100/month - 30 days Trial with $40 signup fee
1039 * {
1040 * 'trial_days' => 30,
1041 * 'repeat_interval' => 'month',
1042 * 'times' => 0, // 0 means unlimited
1043 * 'recurring_amount' => 100,
1044 * 'signup_fee' => 40
1045 * }
1046 *
1047 * $100 / month with $40 signup fee
1048 * {
1049 * 'trial_days' => 0,
1050 * 'repeat_interval' => 'month',
1051 * 'times' => 0, // 0 means unlimited
1052 * 'recurring_amount' => 100,
1053 * 'signup_fee' => 40
1054 * }
1055 *
1056 * $100 per month but 30% discount on first month
1057 * {
1058 * 'trial_days' => 30,
1059 * 'repeat_interval' => 'month',
1060 * 'times' => 0, // 0 means unlimited
1061 * 'recurring_amount' => 100,
1062 * 'signup_fee' => 70
1063 * }
1064 *
1065 * $100 per month but 50% extra on first month
1066 * {
1067 * 'trial_days' => 0,
1068 * 'repeat_interval' => 'month',
1069 * 'times' => 0, // 0 means unlimited
1070 * 'recurring_amount' => 100,
1071 * 'signup_fee' => 50
1072 * }
1073 *
1074 * $100 per month and 1 month trial and service_fee = $50
1075 * {
1076 * 'trial_days' => 30,
1077 * 'repeat_interval' => 'month',
1078 * 'times' => 0, // 0 means unlimited
1079 * 'recurring_amount' => 100,
1080 * 'signup_fee' => 50
1081 * }
1082 *
1083 *
1084 * $100 per month, signup fee $200 - First Month 50% discount
1085 *
1086 * $first Month = 100 + 200 = 300 - 50% discount = 150
1087 * {
1088 * 'trial_days' => 30,
1089 * 'repeat_interval' => 'month',
1090 * 'times' => 0, // 0 means unlimited
1091 * 'recurring_amount' => 100,
1092 * 'signup_fee' => 150
1093 * }
1094 *
1095 * // prefered when signup_fee > recurring_amount
1096 * {
1097 * 'trial_days' => 0,
1098 * 'repeat_interval' => 'month',
1099 * 'times' => 0, // 0 means unlimited
1100 * 'recurring_amount' => 100,
1101 * 'signup_fee' => 50
1102 * }
1103 */
1104
1105
1106 // Extract and validate input data
1107 $trialDays = (int)($inputData['initial_trial_days'] ?? 0);
1108 $repeatInterval = strtolower(trim($inputData['repeat_interval'] ?? 'yearly'));
1109 $times = (int)($inputData['times'] ?? 0);
1110 $recurringAmount = (int)($inputData['recurring_amount'] ?? 0);
1111 $recurringTax = (int)($inputData['recurring_tax_total'] ?? 0);
1112 $signupFee = (int)($inputData['signup_fee'] ?? 0);
1113 $signupFeeTax = (int)($inputData['signup_fee_tax'] ?? 0);
1114 $firstIterationTax = (int)($inputData['first_iteration_tax'] ?? 0);
1115 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
1116
1117 // Determine if THIS subscription item is tax-inclusive (for behavior=3 mixed carts)
1118 $taxBehavior = (int) Arr::get($inputData, 'tax_behavior', 0);
1119 $itemInclusive = (bool) Arr::get($inputData, 'line_meta.tax_config.inclusive', false);
1120 $isAdditiveTax = ($taxBehavior === 1) || ($taxBehavior === 3 && !$itemInclusive);
1121
1122 // Validate repeat_interval
1123 $validIntervals = array_keys(Helper::getAvailableSubscriptionIntervalMaps());
1124
1125 if (!in_array($repeatInterval, $validIntervals)) {
1126 $repeatInterval = 'yearly';
1127 }
1128
1129 // Convert repeat_interval to standard format
1130 $intervalMap = Helper::getAvailableSubscriptionIntervalMaps();
1131 $standardInterval = Helper::translateIntervalToStandardFormat($repeatInterval);
1132
1133 // Calculate trial days based on interval
1134
1135
1136 // Initialize result array
1137 $result = [
1138 'trial_days' => $trialDays,
1139 'repeat_interval' => $standardInterval,
1140 'times' => $times,
1141 ];
1142
1143
1144 // Calculate signup fee logic
1145 if ($totalDiscount > 0) {
1146 // case: discount applied on subscription with trial days and have signup_fee, otherise discount can't be applied on subscription with trial days.
1147 if ($trialDays > 0 && $signupFee > 0) {
1148 $result['signup_fee'] = max(0, $signupFee - $totalDiscount);
1149 $result['manage_setup_fee'] = 'yes';
1150
1151 if ($isAdditiveTax && $firstIterationTax) {
1152 $result['signup_fee'] += $firstIterationTax;
1153 }
1154 } else {
1155 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
1156
1157 if (Arr::get($inputData, 'is_recurring_coupon', 'no') === 'yes') {
1158 $recurringAmount -= $totalDiscount; // as now discount applied on recurring amount
1159 }
1160
1161 if ($firstCycleCost < $recurringAmount) {
1162 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);
1163
1164 $result['trial_days'] = $adjustedTrialDays;
1165 $result['is_trial_days_simulated'] = 'yes';
1166 $result['signup_fee'] = $firstCycleCost;
1167 $result['manage_setup_fee'] = 'yes';
1168 $result['times'] = $times > 0 ? $times - 1 : 0;
1169 } else if ($firstCycleCost > $recurringAmount) {
1170 $result['trial_days'] = 0;
1171 $result['signup_fee'] = $firstCycleCost - $recurringAmount;
1172 $result['manage_setup_fee'] = 'yes';
1173 } else if ($firstCycleCost == $recurringAmount) {
1174 $result['trial_days'] = 0;
1175 $result['signup_fee'] = 0;
1176 $result['manage_setup_fee'] = 'no';
1177 }
1178
1179 // only the signup fee is adjustable on our system, so we can adjust that to our needs
1180 if ($isAdditiveTax && $firstIterationTax) {
1181 if ($result['trial_days'] > 0) {
1182 $result['signup_fee'] += $firstIterationTax;
1183 } else {
1184 $result['signup_fee'] += ($firstIterationTax - $recurringTax); // need to minus the recurring tax as it would add automatically to the payable amount as no trial days
1185 }
1186 }
1187 }
1188
1189 } else {
1190 $result['signup_fee'] = $signupFee;
1191
1192 if ($isAdditiveTax) {
1193 if ($signupFeeTax) {
1194 $result['signup_fee'] += $signupFeeTax;
1195 }
1196 }
1197 }
1198
1199
1200 $result['repeat_interval'] = array_flip($intervalMap)[$result['repeat_interval']] ?? 'yearly';
1201
1202
1203 return [
1204 'billing_interval' => $result['repeat_interval'],
1205 'bill_times' => $result['times'],
1206 'trial_days' => $result['trial_days'],
1207 'is_trial_days_simulated' => Arr::get($result, 'is_trial_days_simulated', 'no'),
1208 'recurring_amount' => $recurringAmount,
1209 'recurring_tax_total' => $recurringTax,
1210 'signup_fee' => $result['signup_fee'] ?? 0,
1211 ];
1212 }
1213 }
1214