PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Helpers / CheckoutProcessor.php

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

1,235 lines 52.7 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
533 /*
534 * Resolve virtual (un-persisted) coupons so an AppliedCoupon row is written for
535 * them too — the row stores coupon_id = null (the column is nullable) with the
536 * code and computed discount, so it shows in the order's Coupons section like any
537 * coupon. See DiscountService::applyCouponCodes() for the same filter.
538 */
539 $coupons = apply_filters('fluent_cart/coupon/resolve_coupons', $coupons, $couponCodes, [
540 'order' => $this->orderModel,
541 ]);
542
543 $coupons = $coupons->keyBy('code')->toArray();
544
545 foreach ($coupons as $code => &$coupon) {
546 $coupon['coupon_id'] = $appliedCoupons[$code]['id'];
547 $coupon['amount'] = $appliedCoupons[$code]['discount'];
548 $coupon['customer_id'] = $customerId;
549 }
550 $this->orderModel->appliedCoupons()->createMany($coupons);
551
552 Coupon::query()
553 ->whereIn('code', $couponCodes)
554 ->increment('use_count', 1);
555 }
556 }
557
558 public function getTransaction()
559 {
560 return $this->transactionModel;
561 }
562
563 public function getOrder()
564 {
565 return $this->orderModel;
566 }
567
568 public function getSubscription()
569 {
570 return $this->subscriptionModel;
571 }
572
573 private function prepareOrderItems()
574 {
575 $formattedItems = [];
576
577 foreach ($this->cartItems as $cartItem) {
578 $unitPrice = (int)Arr::get($cartItem, 'unit_price', 0);
579 $quantity = (int)Arr::get($cartItem, 'quantity', 1);
580
581 $this->couponDiscountTotal += (int)Arr::get($cartItem, 'coupon_discount', 0);
582 $this->manualDiscountTotal += (int)Arr::get($cartItem, 'manual_discount', 0);
583
584 $discountTotal = (int)Arr::get($cartItem, 'manual_discount', 0) + (int)Arr::get($cartItem, 'coupon_discount', 0);
585 $shippingCharge = (int)Arr::get($cartItem, 'shipping_charge', 0);
586
587 $subtotal = (int) Arr::get($cartItem, 'subtotal', $unitPrice * $quantity);
588 $args = Arr::get($cartItem, 'other_info', []);
589 $paymentType = Arr::get($args, 'payment_type', 'default');
590
591 $postTitle = Arr::get($cartItem, 'product_title', '');
592 $variationTitle = Arr::get($cartItem, 'variation_title', '');
593
594 if (!$postTitle) {
595 if (Arr::get($cartItem, 'is_custom', false)) {
596 $postTitle = Arr::get($cartItem, 'post_title', '');
597 } else {
598 $product = Product::query()->find(Arr::get($cartItem, 'post_id', 0));
599 if ($product) {
600 $postTitle = $product->post_title;
601 }
602 }
603 }
604
605 if (!$variationTitle) {
606 if (Arr::get($cartItem, 'is_custom', false)) {
607 $variationTitle = Arr::get($cartItem, 'title', '');
608 } else {
609 $variation = ProductVariation::query()->find(Arr::get($cartItem, 'object_id', 0));
610 if ($variation) {
611 $variationTitle = $variation->variation_title;
612 }
613 }
614 }
615
616 // Snapshot package dimensions into other_info for email/PDF rendering
617 if (Arr::get($cartItem, 'fulfillment_type') === 'physical') {
618 $packageSlug = Arr::get($args, 'package_slug', '');
619 $package = Helper::getPackageBySlug($packageSlug);
620 if ($package) {
621 $args['package_name'] = Arr::get($package, 'name', '');
622 $args['package_type'] = Arr::get($package, 'type', '');
623 $args['package_length'] = Arr::get($package, 'length', '');
624 $args['package_width'] = Arr::get($package, 'width', '');
625 $args['package_height'] = Arr::get($package, 'height', '');
626 $args['package_dimension_unit'] = Arr::get($package, 'dimension_unit', 'cm');
627 $args['package_weight'] = Arr::get($package, 'weight', 0);
628 $args['package_weight_unit'] = Arr::get($package, 'weight_unit', 'kg');
629 }
630 }
631
632 // Carry the attribute snapshot onto the order item. It normally
633 // arrives via the cart item's other_info; rebuild it here as a
634 // fallback for items that reach checkout without one (instant
635 // checkout, legacy carts).
636 if (!isset($args['item_attributes'])) {
637 $args['item_attributes'] = AttributeHelper::getProductItemAttributes(
638 Arr::get($cartItem, 'object_id', 0),
639 Arr::get($cartItem, 'post_id', 0)
640 );
641 }
642
643 $item = [
644 'payment_type' => $paymentType,
645 'post_id' => Arr::get($cartItem, 'post_id'),
646 'object_id' => Arr::get($cartItem, 'object_id'),
647 'post_title' => $postTitle,
648 'title' => $variationTitle,
649 'fulfillment_type' => Arr::get($cartItem, 'fulfillment_type', 'digital'),
650 'quantity' => $quantity,
651 'cost' => (int)Arr::get($cartItem, 'cost', 0),
652 'unit_price' => $unitPrice,
653 'subtotal' => $subtotal,
654 'tax_amount' => (int)Arr::get($cartItem, 'tax_amount', 0),
655 'shipping_charge' => $shippingCharge,
656 'discount_total' => $discountTotal,
657 'coupon_discount' => (int)Arr::get($cartItem, 'coupon_discount', 0),
658 'other_info' => $args,
659 'line_meta' => Arr::get($cartItem, 'line_meta', []),
660 ];
661
662 if (isset($cartItem['recurring_discounts'])) {
663 $item['recurring_discounts'] = $cartItem['recurring_discounts'];
664 }
665
666 $childItem = null;
667 if ($paymentType === 'subscription' && Arr::get($cartItem, 'other_info.signup_fee', 0)) {
668 // We have a signup fee for subscription
669 $signupFeeAmount = (int)Arr::get($cartItem, 'other_info.signup_fee', 0);
670
671 $signupFeeTax = (int)Arr::get($cartItem, 'other_info.signup_fee_tax', 0);
672
673 // Nest under tax_config — the same shape regular items and the admin
674 // order path use. Readers keep a fallback for the legacy flat shape.
675 $signupFeeTaxConfig = Arr::get($cartItem, 'signup_fee_tax_config', []);
676
677 $childDiscountTotal = 0;
678 $childCouponDiscount = 0;
679 $signupFeeSubtotal = $signupFeeAmount * $quantity;
680 $couponDiscount = $item['coupon_discount'];
681
682 $hasTrialDays = Arr::get($cartItem, 'other_info.trial_days', 0) > 0;
683
684 if ($discountTotal && !$hasTrialDays) {
685 $childDiscountTotal = (float)($discountTotal / ($subtotal + $signupFeeSubtotal) * $signupFeeSubtotal);
686 $discountTotal -= $childDiscountTotal;
687 $childCouponDiscount = (int) round($couponDiscount * $signupFeeSubtotal / ($subtotal + $signupFeeSubtotal));
688 $couponDiscount -= $childCouponDiscount;
689 } elseif ($discountTotal && $hasTrialDays) { // if trial days , then discount should be applied on signup fee only
690 $childDiscountTotal = min($discountTotal, $signupFeeSubtotal);
691 $discountTotal = 0;
692 $childCouponDiscount = min($couponDiscount, $signupFeeSubtotal);
693 $couponDiscount = 0;
694 }
695
696 $childItem = [
697 'payment_type' => 'signup_fee',
698 'post_id' => $item['post_id'],
699 'object_id' => $item['object_id'],
700 'post_title' => $item['post_title'],
701 'title' => Arr::get($cartItem, 'other_info.signup_fee_name', __('Signup Fee', 'fluent-cart')),
702 'fulfillment_type' => $item['fulfillment_type'],
703 'quantity' => $quantity,
704 'cost' => 0,
705 'unit_price' => $signupFeeAmount,
706 'subtotal' => $signupFeeSubtotal,
707 'tax_amount' => $signupFeeTax,
708 'shipping_charge' => 0,
709 'discount_total' => $childDiscountTotal,
710 'coupon_discount' => $childCouponDiscount,
711 'line_meta' => $signupFeeTaxConfig ? ['tax_config' => $signupFeeTaxConfig] : [],
712 ];
713
714 $item['discount_total'] = $discountTotal;
715 $item['coupon_discount'] = $couponDiscount;
716 $item['additional_items'] = [$childItem];
717
718 Arr::set($item, 'other_info.signup_fee', $signupFeeAmount);
719 Arr::set($item, 'other_info.signup_discount', $childDiscountTotal);
720 }
721
722 if (
723 Arr::get($cartItem, 'other_info.is_bundle_product', 'no') == 'yes'
724 || !empty(Arr::get($cartItem, 'other_info.bundle_child_ids', []))
725 ) {
726 $bundleItems = Arr::get($cartItem, 'child_variants', []);
727
728 foreach ($bundleItems as $bundleItem) {
729 $bundleChildItem = [
730 'payment_type' => 'bundle',
731 'post_id' => Arr::get($bundleItem, 'post_id', 0),
732 'object_id' => Arr::get($bundleItem, 'id', 0),
733 'post_title' => Arr::get($bundleItem, 'post_title', ''),
734 'title' => Arr::get($bundleItem, 'variation_title', ''),
735 'fulfillment_type' => Arr::get($bundleItem, 'fulfillment_type', 'digital'),
736 'quantity' => $quantity,
737 'cost' => 0,
738 'unit_price' => 0,
739 'subtotal' => 0,
740 'tax_amount' => 0,
741 'shipping_charge' => 0,
742 'discount_total' => 0,
743 'other_info' => [
744 'bundle_parent_product_id' => Arr::get($cartItem, 'post_id', 0),
745 'bundle_parent_variation_id' => Arr::get($cartItem, 'object_id', 0)
746 ],
747 ];
748
749 //TODO: if bundleItem price is included on for the bundle, then we need to set the price to the bundleItem price
750 // if (Arr::get($bundleItem, 'other_info.is_price_included', 'no') == 'yes') {
751 // $bundleChildItem['unit_price'] = Arr::get($bundleItem, 'unit_price', 0);
752 // $bundleChildItem['subtotal'] = Arr::get($bundleItem, 'subtotal', 0);
753 // $bundleChildItem['tax_amount'] = Arr::get($bundleItem, 'tax_amount', 0);
754 // $bundleChildItem['shipping_charge'] = Arr::get($bundleItem, 'shipping_charge', 0);
755 // $bundleChildItem['discount_total'] = Arr::get($bundleItem, 'discount_total', 0);
756 // }
757
758 $item['bundle_items'][] = $bundleChildItem;
759
760 }
761
762 }
763
764 $formattedItems[] = $item;
765 if ($childItem) {
766 $formattedItems[] = $childItem;
767 }
768 }
769
770 // Create order items for fees
771 $fees = (array)Arr::get($this->args, 'fees', []);
772 $this->feeTotal = 0;
773
774 foreach ($fees as $fee) {
775 $amount = (int)($fee['amount'] ?? 0);
776 if ($amount <= 0) {
777 continue;
778 }
779
780 $this->feeTotal += $amount;
781
782 $formattedItems[] = [
783 'payment_type' => 'fee',
784 'post_id' => 0,
785 'object_id' => 0,
786 'post_title' => '',
787 'title' => $fee['label'] ?? '',
788 'fulfillment_type' => 'digital',
789 'quantity' => 1,
790 'cost' => 0,
791 'unit_price' => $amount,
792 'subtotal' => $amount,
793 'tax_amount' => 0,
794 'shipping_charge' => 0,
795 'discount_total' => 0,
796 'other_info' => [
797 'payment_type' => 'fee',
798 'fee_key' => $fee['key'] ?? '',
799 'source' => $fee['source'] ?? 'custom',
800 'taxable' => !empty($fee['taxable']),
801 'meta' => $fee['meta'] ?? [],
802 ],
803 'line_meta' => [],
804 ];
805 }
806
807 $this->formattedIOrderItems = $formattedItems;
808 }
809
810 private function prepareSubscriptionData()
811 {
812 $subscriptionItems = array_filter($this->formattedIOrderItems, function ($item) {
813 return $item['payment_type'] === 'subscription';
814 });
815
816 $signupFeeItems = array_filter($this->formattedIOrderItems, function ($item) {
817 return $item['payment_type'] === 'signup_fee';
818 });
819
820 if (!$subscriptionItems) {
821 return;
822 }
823
824 if (count($subscriptionItems) > 1) {
825 return;
826 }
827
828 $item = reset($subscriptionItems);
829 $signupFeeItem = reset($signupFeeItems) ?? [];
830 $signupFeeTax = (int)Arr::get($signupFeeItem, 'tax_amount', 0);
831 $taxBehavior = Arr::get($this->args, 'tax_behavior', 0);
832
833 $recurringTotal = (int)$item['subtotal'];
834 $recurringTax = (int)Arr::get($item, 'other_info.recurring_tax', 0);
835
836 $recurringDiscountAmount = (int)Arr::get($item, 'recurring_discounts.amount', 0);
837
838 if ($recurringDiscountAmount && $recurringDiscountAmount > 0) {
839 $recurringTotal -= $recurringDiscountAmount;
840 }
841
842 // Add shipping charges to recurring total for physical subscription products
843 $shippingCharge = (int)Arr::get($this->args, 'shipping_charge', 0);
844 $isPhysicalProduct = Arr::get($item, 'fulfillment_type') === 'physical';
845 if ($isPhysicalProduct && $shippingCharge > 0) {
846 $recurringTotal += $shippingCharge;
847 }
848
849 $itemInclusive = (bool) Arr::get($item, 'line_meta.tax_config.inclusive', false);
850 if ($taxBehavior === 1 || ($taxBehavior === 3 && !$itemInclusive)) {
851 $recurringTotal += $recurringTax;
852 }
853
854 $signupFee = (int)Arr::get($signupFeeItem, 'subtotal', 0);
855
856 // in case of discount applied 'tax_amount' is different than recurring tax ,
857 $firstIterationTax = (int)Arr::get($item, 'tax_amount', 0) + $signupFeeTax;
858
859
860 // Calculate recurring amount including shipping for physical products
861 $recurringAmount = (int)$item['subtotal'];
862 if ($isPhysicalProduct && $shippingCharge > 0) {
863 $recurringAmount += $shippingCharge;
864 }
865
866 $discountTotal = $item['discount_total'] + Arr::get($signupFeeItem, 'discount_total', 0) + $this->prorateCreditTotal + $this->upgradeDiscountTotal;
867 $subscriptionPricing = $this->convertToSubscriptionFormat([
868 'initial_trial_days' => Arr::get($item, 'other_info.trial_days', 0),
869 'repeat_interval' => Arr::get($item, 'other_info.repeat_interval', 'monthly'),
870 'times' => Arr::get($item, 'other_info.times', 0),
871 'recurring_amount' => $recurringAmount,
872 'recurring_tax_total' => $recurringTax,
873 'recurring_total' => $recurringTotal,
874 'tax_behavior' => $taxBehavior,
875 'line_meta' => Arr::get($item, 'line_meta', []),
876 'signup_fee' => $signupFee,
877 'signup_fee_tax' => $signupFeeTax,
878 'first_iteration_tax' => $firstIterationTax,
879 'is_recurring_coupon' => Arr::get($item, 'is_recurring_coupon', 'no'),
880 'total_discount' => $discountTotal
881 ]);
882
883 // removable upon discussion
884 $subscriptionItem = [
885 'product_id' => $item['post_id'],
886 'current_payment_method' => Arr::get($this->orderData, 'payment_method'),
887 'object_id' => $item['object_id'],
888 'recurring_tax_total' => 0,
889 'recurring_total' => $recurringTotal, //use price not line_total to ignore discount
890 'item_name' => $item['post_title'] . ' - ' . $item['title'],
891 'bill_count' => 0,
892 'quantity' => 1,
893 'variation_id' => Arr::get($item, 'object_id', 0),
894 'status' => Status::SUBSCRIPTION_PENDING,
895 'config' => [
896 'is_trial_days_simulated' => Arr::get($subscriptionPricing, 'is_trial_days_simulated', 'no'),
897 'currency' => $this->orderData['currency']
898 ]
899 ];
900
901 // if recurring coupon is applied, we need to subtract the total discount from the recurring total
902 if (Arr::get($item, 'is_recurring_coupon', 'no') === 'yes') {
903 $subscriptionItem['recurring_total'] -= $discountTotal;
904 }
905
906 $this->subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
907 }
908
909 private function prepareOrderData()
910 {
911 $hasPhysical = array_filter($this->formattedIOrderItems, function ($item) {
912 return $item['fulfillment_type'] === 'physical';
913 });
914
915 $hasSubscription = array_filter($this->formattedIOrderItems, function ($item) {
916 return $item['payment_type'] === 'subscription';
917 });
918
919 $itemsSubtotal = array_reduce($this->formattedIOrderItems, function ($carry, $item) {
920 if (Arr::get($item, 'other_info.trial_days', 0) > 0) {
921 return $carry;
922 }
923 // Fee items are tracked separately via fee_total
924 if (Arr::get($item, 'payment_type') === 'fee') {
925 return $carry;
926 }
927 return $carry + $item['subtotal'];
928 }, 0);
929
930 $taxBehavior = (int) Arr::get($this->args, 'tax_behavior', 0);
931 $storeTaxBehavior = (int) Arr::get($this->args, 'store_tax_behavior', $taxBehavior);
932 $exclusiveTaxTotal = (int) Arr::get($this->args, 'exclusive_tax_total', 0);
933 $feeTax = (int) Arr::get($this->args, 'fee_tax', 0);
934
935 // Roll fee tax into fee_total for exclusive scenarios — gateways use fee_total as source of truth.
936 if ($feeTax && ($taxBehavior === 1 || ($taxBehavior === 3 && $storeTaxBehavior === 1))) {
937 $this->feeTotal += $feeTax;
938 }
939
940 $this->prorateCreditTotal = (int) Arr::get($this->args, 'prorate_credit', 0);
941 // Upgrade-path discount is a post-tax adjustment like the prorate credit: it does
942 // not reduce the taxable base (tax args were computed on the full price), it only
943 // reduces the payable total via manual_discount_total below.
944 $this->upgradeDiscountTotal = (int) Arr::get($this->args, 'upgrade_discount', 0);
945
946 $orderData = [
947 'status' => Status::ORDER_ON_HOLD,
948 'fulfillment_type' => $hasPhysical ? Status::FULFILLMENT_TYPE_PHYSICAL : Status::FULFILLMENT_TYPE_DIGITAL,
949 'type' => $hasSubscription ? Status::ORDER_TYPE_SUBSCRIPTION : Status::ORDER_TYPE_PAYMENT, // revisit this on manual renewal
950 'mode' => $this->storeSettings->get('order_mode', 'test'),
951 'shipping_status' => $hasPhysical ? 'unshipped' : '',
952 'customer_id' => '',
953 'payment_method' => Arr::get($this->args, 'payment_method', ''),
954 'payment_status' => Status::PAYMENT_PENDING,
955 'payment_method_title' => '',
956 'currency' => $this->storeSettings->get('currency'),
957 'subtotal' => $itemsSubtotal,
958 'discount_tax' => 0,
959 'manual_discount_total' => $this->manualDiscountTotal + $this->prorateCreditTotal + $this->upgradeDiscountTotal,
960 'coupon_discount_total' => $this->couponDiscountTotal,
961 'shipping_tax' => Arr::get($this->args, 'shipping_tax', 0),
962 'shipping_total' => Arr::get($this->args, 'shipping_charge', 0),
963 'fee_total' => $this->feeTotal,
964 'tax_total' => Arr::get($this->args, 'tax_total', 0),
965 'tax_behavior' => $taxBehavior,
966 // 'total_amount' => $this->orderTotals['total_amount'],
967 'total_paid' => 0,
968 'total_refund' => 0,
969 'rate' => 1,
970 'note' => Arr::get($this->args, 'note', ''),
971 'ip_address' => Arr::get($this->args, 'ip_address', ''),
972 'config' => [
973 'user_tz' => Arr::get($this->args, 'user_tz', ''),
974 'create_account_after_paid' => Arr::get($this->args, 'create_account_after_paid', 'no'),
975 'shipping_method_id' => Arr::get($this->args, 'shipping_method_id', 0),
976 'shipping_method_title' => Arr::get($this->args, 'shipping_method_title', ''),
977 'prorate_credit' => $this->prorateCreditTotal,
978 'upgrade_discount' => $this->upgradeDiscountTotal,
979 ],
980 ];
981
982 if ($taxBehavior === 1) {
983 // Pure exclusive: fee_tax is already in fee_total; exclude it here to avoid double-count.
984 $estimatedTaxTotal = $orderData['tax_total'] - $feeTax;
985 $estimatedShippingTax = $orderData['shipping_tax'];
986 } elseif ($taxBehavior === 3) {
987 // Mixed: fee_tax rolled into fee_total for exclusive store; shipping still additive.
988 $estimatedTaxTotal = $exclusiveTaxTotal;
989 $estimatedShippingTax = ($storeTaxBehavior === 1) ? $orderData['shipping_tax'] : 0;
990 } else {
991 // Inclusive (2) or reverse-charge (0): nothing to add to total; tax is in item prices.
992 $estimatedTaxTotal = 0;
993 $estimatedShippingTax = 0;
994 if ($taxBehavior !== 2) {
995 // Reverse-charge (0): zero stored columns — no tax applies.
996 // Inclusive (2): keep orderData values so reporting surfaces can read them.
997 $orderData['tax_total'] = 0;
998 $orderData['shipping_tax'] = 0;
999 }
1000 }
1001
1002 $totalAmount = $orderData['subtotal']
1003 - $orderData['coupon_discount_total']
1004 - $orderData['manual_discount_total']
1005 + $orderData['fee_total']
1006 + $orderData['shipping_total']
1007 + $estimatedTaxTotal
1008 + $estimatedShippingTax;
1009
1010 $orderData['total_amount'] = $totalAmount > 0 ? $totalAmount : 0;
1011 $this->orderData = $orderData;
1012 }
1013
1014 private function syncFeeItems()
1015 {
1016 $orderId = $this->orderModel->id;
1017
1018 // Remove existing fee items
1019 OrderItem::query()->where('order_id', $orderId)->where('payment_type', 'fee')->delete();
1020
1021 // Create new fee items from formatted items
1022 $feeItems = array_filter($this->formattedIOrderItems, function ($item) {
1023 return $item['payment_type'] === 'fee';
1024 });
1025
1026 foreach ($feeItems as $feeItem) {
1027 $feeItem['order_id'] = $orderId;
1028 $feeItem['quantity'] = 1;
1029 $feeItem['line_total'] = $feeItem['subtotal'] - $feeItem['discount_total'];
1030 OrderItem::query()->create($feeItem);
1031 }
1032 }
1033
1034 /**
1035 * @param $inputData
1036 * @return array
1037 */
1038 private function convertToSubscriptionFormat($inputData)
1039 {
1040
1041 /**
1042 * Normal Subscription $100/month
1043 * {
1044 * 'trial_days' => 0,
1045 * 'repeat_interval' => 'month',
1046 * 'times' => 0, // 0 means unlimited
1047 * 'recurring_amount' => 100,
1048 * 'signup_fee' => 0
1049 * }
1050 *
1051 * 30 Days Trial $100/month
1052 * {
1053 * 'trial_days' => 30,
1054 * 'repeat_interval' => 'month',
1055 * 'times' => 0, // 0 means unlimited
1056 * 'recurring_amount' => 100,
1057 * }
1058 *
1059 * $100/month - 30 days Trial with $40 signup fee
1060 * {
1061 * 'trial_days' => 30,
1062 * 'repeat_interval' => 'month',
1063 * 'times' => 0, // 0 means unlimited
1064 * 'recurring_amount' => 100,
1065 * 'signup_fee' => 40
1066 * }
1067 *
1068 * $100 / month with $40 signup fee
1069 * {
1070 * 'trial_days' => 0,
1071 * 'repeat_interval' => 'month',
1072 * 'times' => 0, // 0 means unlimited
1073 * 'recurring_amount' => 100,
1074 * 'signup_fee' => 40
1075 * }
1076 *
1077 * $100 per month but 30% discount on first month
1078 * {
1079 * 'trial_days' => 30,
1080 * 'repeat_interval' => 'month',
1081 * 'times' => 0, // 0 means unlimited
1082 * 'recurring_amount' => 100,
1083 * 'signup_fee' => 70
1084 * }
1085 *
1086 * $100 per month but 50% extra on first month
1087 * {
1088 * 'trial_days' => 0,
1089 * 'repeat_interval' => 'month',
1090 * 'times' => 0, // 0 means unlimited
1091 * 'recurring_amount' => 100,
1092 * 'signup_fee' => 50
1093 * }
1094 *
1095 * $100 per month and 1 month trial and service_fee = $50
1096 * {
1097 * 'trial_days' => 30,
1098 * 'repeat_interval' => 'month',
1099 * 'times' => 0, // 0 means unlimited
1100 * 'recurring_amount' => 100,
1101 * 'signup_fee' => 50
1102 * }
1103 *
1104 *
1105 * $100 per month, signup fee $200 - First Month 50% discount
1106 *
1107 * $first Month = 100 + 200 = 300 - 50% discount = 150
1108 * {
1109 * 'trial_days' => 30,
1110 * 'repeat_interval' => 'month',
1111 * 'times' => 0, // 0 means unlimited
1112 * 'recurring_amount' => 100,
1113 * 'signup_fee' => 150
1114 * }
1115 *
1116 * // prefered when signup_fee > recurring_amount
1117 * {
1118 * 'trial_days' => 0,
1119 * 'repeat_interval' => 'month',
1120 * 'times' => 0, // 0 means unlimited
1121 * 'recurring_amount' => 100,
1122 * 'signup_fee' => 50
1123 * }
1124 */
1125
1126
1127 // Extract and validate input data
1128 $trialDays = (int)($inputData['initial_trial_days'] ?? 0);
1129 $repeatInterval = strtolower(trim($inputData['repeat_interval'] ?? 'yearly'));
1130 $times = (int)($inputData['times'] ?? 0);
1131 $recurringAmount = (int)($inputData['recurring_amount'] ?? 0);
1132 $recurringTax = (int)($inputData['recurring_tax_total'] ?? 0);
1133 $signupFee = (int)($inputData['signup_fee'] ?? 0);
1134 $signupFeeTax = (int)($inputData['signup_fee_tax'] ?? 0);
1135 $firstIterationTax = (int)($inputData['first_iteration_tax'] ?? 0);
1136 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
1137
1138 // Determine if THIS subscription item is tax-inclusive (for behavior=3 mixed carts)
1139 $taxBehavior = (int) Arr::get($inputData, 'tax_behavior', 0);
1140 $itemInclusive = (bool) Arr::get($inputData, 'line_meta.tax_config.inclusive', false);
1141 $isAdditiveTax = ($taxBehavior === 1) || ($taxBehavior === 3 && !$itemInclusive);
1142
1143 // Validate repeat_interval
1144 $validIntervals = array_keys(Helper::getAvailableSubscriptionIntervalMaps());
1145
1146 if (!in_array($repeatInterval, $validIntervals)) {
1147 $repeatInterval = 'yearly';
1148 }
1149
1150 // Convert repeat_interval to standard format
1151 $intervalMap = Helper::getAvailableSubscriptionIntervalMaps();
1152 $standardInterval = Helper::translateIntervalToStandardFormat($repeatInterval);
1153
1154 // Calculate trial days based on interval
1155
1156
1157 // Initialize result array
1158 $result = [
1159 'trial_days' => $trialDays,
1160 'repeat_interval' => $standardInterval,
1161 'times' => $times,
1162 ];
1163
1164
1165 // Calculate signup fee logic
1166 if ($totalDiscount > 0) {
1167 // case: discount applied on subscription with trial days and have signup_fee, otherise discount can't be applied on subscription with trial days.
1168 if ($trialDays > 0 && $signupFee > 0) {
1169 $result['signup_fee'] = max(0, $signupFee - $totalDiscount);
1170 $result['manage_setup_fee'] = 'yes';
1171
1172 if ($isAdditiveTax && $firstIterationTax) {
1173 $result['signup_fee'] += $firstIterationTax;
1174 }
1175 } else {
1176 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
1177
1178 if (Arr::get($inputData, 'is_recurring_coupon', 'no') === 'yes') {
1179 $recurringAmount -= $totalDiscount; // as now discount applied on recurring amount
1180 }
1181
1182 if ($firstCycleCost < $recurringAmount) {
1183 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);
1184
1185 $result['trial_days'] = $adjustedTrialDays;
1186 $result['is_trial_days_simulated'] = 'yes';
1187 $result['signup_fee'] = $firstCycleCost;
1188 $result['manage_setup_fee'] = 'yes';
1189 $result['times'] = $times > 0 ? $times - 1 : 0;
1190 } else if ($firstCycleCost > $recurringAmount) {
1191 $result['trial_days'] = 0;
1192 $result['signup_fee'] = $firstCycleCost - $recurringAmount;
1193 $result['manage_setup_fee'] = 'yes';
1194 } else if ($firstCycleCost == $recurringAmount) {
1195 $result['trial_days'] = 0;
1196 $result['signup_fee'] = 0;
1197 $result['manage_setup_fee'] = 'no';
1198 }
1199
1200 // only the signup fee is adjustable on our system, so we can adjust that to our needs
1201 if ($isAdditiveTax && $firstIterationTax) {
1202 if ($result['trial_days'] > 0) {
1203 $result['signup_fee'] += $firstIterationTax;
1204 } else {
1205 $result['signup_fee'] += ($firstIterationTax - $recurringTax); // need to minus the recurring tax as it would add automatically to the payable amount as no trial days
1206 }
1207 }
1208 }
1209
1210 } else {
1211 $result['signup_fee'] = $signupFee;
1212
1213 if ($isAdditiveTax) {
1214 if ($signupFeeTax) {
1215 $result['signup_fee'] += $signupFeeTax;
1216 }
1217 }
1218 }
1219
1220
1221 $result['repeat_interval'] = array_flip($intervalMap)[$result['repeat_interval']] ?? 'yearly';
1222
1223
1224 return [
1225 'billing_interval' => $result['repeat_interval'],
1226 'bill_times' => $result['times'],
1227 'trial_days' => $result['trial_days'],
1228 'is_trial_days_simulated' => Arr::get($result, 'is_trial_days_simulated', 'no'),
1229 'recurring_amount' => $recurringAmount,
1230 'recurring_tax_total' => $recurringTax,
1231 'signup_fee' => $result['signup_fee'] ?? 0,
1232 ];
1233 }
1234 }
1235