PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.2
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.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.5.2, at app/Helpers/CheckoutProcessor.php

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