PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.3
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 / AdminOrderProcessor.php

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

737 lines 30.9 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\StoreSettings;
6 use FluentCart\App\Models\Cart;
7 use FluentCart\App\Models\Coupon;
8 use FluentCart\App\Models\Order;
9 use FluentCart\App\Models\OrderItem;
10 use FluentCart\App\Models\Product;
11 use FluentCart\App\Models\ProductVariation;
12 use FluentCart\App\Models\Subscription;
13 use FluentCart\App\Services\Payments\PaymentHelper;
14 use FluentCart\Framework\Support\Arr;
15 use FluentCart\App\Helpers\Helper;
16
17 class AdminOrderProcessor
18 {
19 private $args = [];
20 private $checkoutItems = [];
21
22 // Order Related Data
23 private $formattedIOrderItems = [];
24 private $orderData = [];
25 private $subscriptionData = [];
26
27 // Models
28
29 private $orderModel;
30
31 private $transactionModel;
32
33 private $subscriptionModel;
34
35 // Store Settings
36 private $storeSettings;
37
38
39 private $couponDiscountTotal = 0;
40
41 private $manualDiscountTotal = 0;
42
43 public function __construct($checkoutItems = [], $args = [])
44 {
45 $this->storeSettings = new StoreSettings();
46 $this->checkoutItems = $checkoutItems;
47 $this->checkoutItems = Helper::loadBundleChild($checkoutItems, ['*']);
48 $this->args = $args;
49
50 $this->prepareData();
51 }
52
53 private function prepareData()
54 {
55 $this->prepareOrderItems();
56 $this->prepareOrderData();
57 $this->prepareSubscriptionData();
58 }
59
60 private function prepareOrderItems()
61 {
62 $formattedItems = [];
63
64 foreach ($this->checkoutItems as $checkoutItem) {
65 $unitPrice = (int)Arr::get($checkoutItem, 'unit_price', 0);
66 $quantity = (int)Arr::get($checkoutItem, 'quantity', 1);
67
68 //TODO: right now discount total from admin only comes from coupon applied discount, is same as coupon_discount
69 $this->couponDiscountTotal += (int)Arr::get($checkoutItem, 'discount_total', 0);
70 $this->manualDiscountTotal += (int)Arr::get($checkoutItem, 'manual_discount', 0);
71
72 $discountTotal = (int)Arr::get($checkoutItem, 'manual_discount', 0) + (int)Arr::get($checkoutItem, 'discount_total', 0);
73
74 $shippingCharge = (int)Arr::get($checkoutItem, 'shipping_charge', 0);
75 $tax = 0;
76 $subtotal = $unitPrice * $quantity;
77 $args = Arr::get($checkoutItem, 'other_info', []);
78 $paymentType = Arr::get($args, 'payment_type', 'default');
79
80 $postTitle = Arr::get($checkoutItem, 'product_title', '');
81 $variationTitle = Arr::get($checkoutItem, 'variation_title', '');
82
83 if (!$postTitle) {
84 $product = Product::query()->find(Arr::get($checkoutItem, 'post_id', 0));
85 if ($product) {
86 $postTitle = $product->post_title;
87 }
88 }
89
90 $variation = null;
91 if (!$variationTitle) {
92 $variation = ProductVariation::query()->find(Arr::get($checkoutItem, 'object_id', 0));
93 if ($variation) {
94 $variationTitle = $variation->variation_title;
95 }
96 }
97
98 if (!$variation) {
99 $variation = ProductVariation::query()->find(Arr::get($checkoutItem, 'object_id', 0));
100 }
101
102 $fulfillmentType = Arr::get($checkoutItem, 'fulfillment_type', '');
103 if (!$fulfillmentType && $variation) {
104 $fulfillmentType = $variation->fulfillment_type;
105 }
106 if (!$fulfillmentType) {
107 $fulfillmentType = 'digital';
108 }
109
110 // Snapshot the variant's attribute set so admin-created orders carry
111 // the same pa_* attribute map as storefront orders.
112 if (!isset($args['item_attributes'])) {
113 $args['item_attributes'] = AttributeHelper::getProductItemAttributes(
114 Arr::get($checkoutItem, 'object_id', 0),
115 Arr::get($checkoutItem, 'post_id', 0)
116 );
117 }
118
119 if (!isset($args['variation_type'])) {
120 $args['variation_type'] = ($variation && $variation->product_detail) ? $variation->product_detail->variation_type : '';
121 }
122
123 $item = [
124 'payment_type' => $paymentType,
125 'post_id' => Arr::get($checkoutItem, 'post_id'),
126 'object_id' => Arr::get($checkoutItem, 'object_id'),
127 'post_title' => $postTitle,
128 'title' => $variationTitle,
129 'fulfillment_type' => $fulfillmentType,
130 'quantity' => $quantity,
131 'cost' => (int)Arr::get($checkoutItem, 'cost', 0),
132 'unit_price' => $unitPrice,
133 'subtotal' => $subtotal,
134 'tax_amount' => $tax,
135 'shipping_charge' => $shippingCharge,
136 'discount_total' => $discountTotal,
137 'coupon_discount' => (int)Arr::get($checkoutItem, 'discount_total', 0),
138 'other_info' => $args,
139 'line_meta' => []
140 ];
141
142 $childItem = null;
143 if ($paymentType === 'subscription' && Arr::get($checkoutItem, 'other_info.signup_fee', 0)) {
144 // We have a signup fee for subscription
145 $singupFeeAmount = (int)Arr::get($checkoutItem, 'other_info.signup_fee', 0);
146
147 $childDiscontTotal = 0;
148 $childCouponDiscount = 0;
149 $signupFeeSubtotal = $singupFeeAmount * $quantity;
150 $couponDiscount = $item['coupon_discount'];
151
152 if ($discountTotal) {
153 // Distribute discount with signup fee and item
154 $childDiscontTotal = (int)($discountTotal / ($subtotal + $signupFeeSubtotal) * $signupFeeSubtotal);
155 $discountTotal -= $childDiscontTotal;
156 $childCouponDiscount = (int) round($couponDiscount * $signupFeeSubtotal / ($subtotal + $signupFeeSubtotal));
157 $couponDiscount -= $childCouponDiscount;
158 }
159
160 $childItem = [
161 'payment_type' => 'signup_fee',
162 'post_id' => $item['post_id'],
163 'object_id' => $item['object_id'],
164 'post_title' => $item['post_title'],
165 'title' => Arr::get($checkoutItem, 'other_info.signup_fee_name', __('Signup Fee', 'fluent-cart')),
166 'fulfillment_type' => $item['fulfillment_type'],
167 'quantity' => $quantity,
168 'cost' => 0,
169 'unit_price' => $singupFeeAmount,
170 'subtotal' => $signupFeeSubtotal,
171 'tax_amount' => 0,
172 'shipping_charge' => 0,
173 'discount_total' => $childDiscontTotal,
174 'coupon_discount' => $childCouponDiscount,
175 'line_meta' => []
176 ];
177
178 $item['discount_total'] = $discountTotal;
179 $item['coupon_discount'] = $couponDiscount;
180 $item['additional_items'] = [$childItem];
181
182 Arr::set($item, 'other_info.signup_fee', $singupFeeAmount);
183 Arr::set($item, 'other_info.signup_discount', $childDiscontTotal);
184 }
185
186 if (Arr::get($checkoutItem, 'other_info.is_bundle_product', 'no') == 'yes') {
187 $bundleItems = Arr::get($checkoutItem, 'child_variants', []);
188
189 foreach ($bundleItems as $bundleItem) {
190 $bundleChildItem = [
191 'payment_type' => 'bundle',
192 'post_id' => Arr::get($bundleItem, 'post_id', 0),
193 'object_id' => Arr::get($bundleItem, 'id', 0),
194 'post_title' => Arr::get($bundleItem, 'post_title', ''),
195 'title' => Arr::get($bundleItem, 'variation_title', ''),
196 'fulfillment_type' => Arr::get($bundleItem, 'fulfillment_type', 'digital'),
197 'quantity' => $quantity,
198 'cost' => 0,
199 'unit_price' => 0,
200 'subtotal' => 0,
201 'tax_amount' => 0,
202 'shipping_charge' => 0,
203 'discount_total' => 0,
204 'other_info' => [
205 'bundle_parent_product_id' => Arr::get($checkoutItem, 'post_id', 0),
206 'bundle_parent_variation_id' => Arr::get($checkoutItem, 'object_id', 0)
207 ],
208 ];
209
210 //TODO: if bundleItem price is included on for the bundle, then we need to set the price to the bundleItem price
211 // if (Arr::get($bundleItem, 'other_info.is_price_included', 'no') == 'yes') {
212 // $bundleChildItem['unit_price'] = Arr::get($bundleItem, 'unit_price', 0);
213 // $bundleChildItem['subtotal'] = Arr::get($bundleItem, 'subtotal', 0);
214 // $bundleChildItem['tax_amount'] = Arr::get($bundleItem, 'tax_amount', 0);
215 // $bundleChildItem['shipping_charge'] = Arr::get($bundleItem, 'shipping_charge', 0);
216 // $bundleChildItem['discount_total'] = Arr::get($bundleItem, 'discount_total', 0);
217 // }
218
219 $item['bundle_items'][] = $bundleChildItem;
220
221 }
222
223 }
224
225
226 $formattedItems[] = $item;
227 if ($childItem) {
228 $formattedItems[] = $childItem;
229 }
230 }
231
232 $this->formattedIOrderItems = $formattedItems;
233 }
234
235 private function prepareOrderData()
236 {
237 $hasPhysical = array_filter($this->formattedIOrderItems, function ($item) {
238 return $item['fulfillment_type'] === 'physical';
239 });
240
241 $hasSubscription = array_filter($this->formattedIOrderItems, function ($item) {
242 return $item['payment_type'] === 'subscription';
243 });
244
245
246 $itemsSubtotal = array_reduce($this->formattedIOrderItems, function ($carry, $item) {
247 if (Arr::get($item, 'other_info.trial_days', 0) > 0) {
248 return $carry;
249 }
250 return $carry + $item['subtotal'];
251 }, 0);
252
253 $orderData = [
254 'status' => Status::ORDER_ON_HOLD,
255 'fulfillment_type' => $hasPhysical ? Status::FULFILLMENT_TYPE_PHYSICAL : Status::FULFILLMENT_TYPE_DIGITAL,
256 'type' => $hasSubscription ? Status::ORDER_TYPE_SUBSCRIPTION : Status::ORDER_TYPE_PAYMENT, // revisit this on manual renewal
257 'mode' => $this->storeSettings->get('order_mode'),
258 'shipping_status' => $hasPhysical ? 'unshipped' : '',
259 'customer_id' => '',
260 'payment_method' => Arr::get($this->args, 'payment_method', ''),
261 'payment_status' => Status::PAYMENT_PENDING,
262 'payment_method_title' => '',
263 'currency' => $this->storeSettings->get('currency'),
264 'subtotal' => $itemsSubtotal,
265 'discount_tax' => 0,
266 'manual_discount_total' => $this->manualDiscountTotal,
267 'coupon_discount_total' => $this->couponDiscountTotal,
268 'shipping_tax' => 0,
269 'shipping_total' => Arr::get($this->args, 'shipping_total', 0),
270 'tax_total' => 0,
271 // 'total_amount' => $this->orderTotals['total_amount'],
272 'total_paid' => 0,
273 'total_refund' => 0,
274 'rate' => 1,
275 'note' => Arr::get($this->args, 'note', ''),
276 'ip_address' => Arr::get($this->args, 'ip_address', ''),
277 'config' => [
278 'user_tz' => Arr::get($this->args, 'user_tz', ''),
279 ],
280 ];
281
282 $totalAmount = $orderData['subtotal'] - $orderData['coupon_discount_total'] - $orderData['manual_discount_total'] + $orderData['shipping_total'] + $orderData['tax_total'];
283 $orderData['total_amount'] = $totalAmount > 0 ? $totalAmount : 0;
284 $this->orderData = $orderData;
285 }
286
287
288 public function createDraftOrder($prevOrder = null)
289 {
290 $customerId = Arr::get($this->args, 'customer_id', '');
291 if (!$customerId) {
292 return new \WP_Error('customer_id_missing', __('Customer ID is required to create a draft order.', 'fluent-cart'));
293 }
294
295 $orderData = $this->orderData;
296 $orderData['customer_id'] = $customerId;
297 $this->orderModel = \FluentCart\App\Models\Order::query()->create($orderData);
298
299 if (!$this->orderModel) {
300 return new \WP_Error('order_creation_failed', __('Failed to create order.', 'fluent-cart'));
301 }
302
303 // Let's create the order items
304 $normalOrderItems = array_filter($this->formattedIOrderItems, function ($item) {
305 return $item['payment_type'] != 'signup_fee';
306 });
307
308
309 foreach ($normalOrderItems as $orderItem) {
310 $orderItem['order_id'] = $this->orderModel->id;
311 $orderItem['line_total'] = $orderItem['subtotal'] - $orderItem['discount_total'];
312 $additionalItems = [];
313 $bundleItems = [];
314 if ($orderItem['payment_type'] == 'subscription') {
315 // this is a subscription type. We may have additional_items
316 $additionalItems = Arr::get($orderItem, 'additional_items', []);
317 unset($orderItem['additional_items']);
318 }
319
320 if (Arr::get($orderItem, 'other_info.is_bundle_product', 'no') == 'yes') {
321 $bundleItems = Arr::get($orderItem, 'bundle_items', []);
322 unset($orderItem['bundle_items']);
323 }
324
325 if (!empty($orderItem['coupon_discount'])) {
326 $lineMeta = Arr::get($orderItem, 'line_meta', []);
327 $lineMeta['coupon_discount'] = (int) $orderItem['coupon_discount'];
328 $orderItem['line_meta'] = $lineMeta;
329 }
330
331 $createdItem = OrderItem::query()->create($orderItem);
332
333 if ($additionalItems) {
334 $additionalItemIds = [];
335 foreach ($additionalItems as $additionalItem) {
336 $additionalItem['order_id'] = $this->orderModel->id;
337 $additionalItem['line_total'] = $additionalItem['subtotal'] - $additionalItem['discount_total'];
338 $mata = Arr::get($additionalItem, 'line_meta', []);
339 $mata['parent_item_id'] = $createdItem->id;
340 if (!empty($additionalItem['coupon_discount'])) {
341 $mata['coupon_discount'] = (int) $additionalItem['coupon_discount'];
342 }
343 $additionalItem['line_meta'] = $mata;
344 OrderItem::query()->create($additionalItem);
345 }
346
347 $createdItem->fill([
348 'line_meta' => array_merge(
349 $createdItem->line_meta,
350 [
351 'additional_item_ids' => $additionalItemIds
352 ]
353 )
354 ])->save();
355 }
356
357 if ($bundleItems) {
358 $bundleItemIds = [];
359 foreach ($bundleItems as $bundleItem) {
360 $bundleItem['order_id'] = $this->orderModel->id;
361 $bundleItem['line_total'] = Arr::get($bundleItem, 'subtotal', 0) - Arr::get($bundleItem, 'discount_total', 0);
362 $bundleItem['payment_type'] = 'bundle';
363 $meta = Arr::get($bundleItem, 'line_meta', []);
364 $meta['bundle_parent_item_id'] = $createdItem->id;
365 if (!empty($bundleItem['coupon_discount'])) {
366 $meta['coupon_discount'] = (int) $bundleItem['coupon_discount'];
367 }
368 $bundleItem['line_meta'] = $meta;
369 $bundleItem = OrderItem::query()->create($bundleItem);
370 $bundleItemIds[] = $bundleItem->id;
371 }
372
373 $createdItem->fill([
374 'line_meta' => array_merge(
375 $createdItem->line_meta,
376 [
377 'bundle_item_ids' => $bundleItemIds
378 ]
379 )
380 ])->save();
381 }
382 }
383
384 // Let's create the subscription if exists
385 if ($this->subscriptionData) {
386 $subscriptionData = $this->subscriptionData;
387 $subscriptionData['customer_id'] = $customerId;
388 $subscriptionData['parent_order_id'] = $this->orderModel->id;
389 $this->subscriptionModel = Subscription::query()->create($subscriptionData);
390
391 // bill_count counting can't tell "billed cycle" from "something else
392 // charged alongside it." Must be decided at creation: payment-method switching
393 // also sets is_trial_days_simulated, so the flag alone can't be trusted at runtime.
394 $isSimulated = Arr::get($subscriptionData, 'config.is_trial_days_simulated', 'no') === 'yes';
395 $trialDays = (int)Arr::get($subscriptionData, 'trial_days', 0);
396 $billTimes = (int)$this->subscriptionModel->bill_times;
397 $orderTotal = (int)$this->orderModel->total_amount;
398
399 // Simulated trial, $0 first cycle: consumes a cycle but produces no
400 // total > 0 transaction — add billed_cycles_offset so it still counts.
401 if ($isSimulated
402 && $billTimes > 0
403 && (int)$this->subscriptionModel->signup_fee <= 0
404 && !$orderTotal
405 ) {
406 $this->subscriptionModel->updateMeta('billed_cycles_offset', 1);
407 }
408
409 // Real trial with a signup fee: the initial charge is the signup fee only,
410 // but it IS a total > 0 transaction linked to the subscription — mark
411 // billed_cycles_deduction so it does NOT count as a cycle.
412 if (!$isSimulated
413 && $trialDays > 0
414 && $billTimes > 0
415 && $orderTotal > 0
416 ) {
417 $this->subscriptionModel->updateMeta('billed_cycles_deduction', 1);
418 }
419 }
420
421 // Let's create the transaction
422 $transactionData = [
423 'order_id' => $this->orderModel->id,
424 'order_type' => $this->orderModel->type,
425 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
426 'subscription_id' => $this->subscriptionModel ? $this->subscriptionModel->id : NULL,
427 'payment_method' => $this->orderModel->payment_method,
428 'payment_mode' => $this->orderModel->mode,
429 'payment_method_type' => '',
430 'status' => Status::PAYMENT_PENDING,
431 'currency' => $this->orderModel->currency,
432 'total' => $this->orderModel->total_amount,
433 'rate' => 1,
434 'meta' => [],
435 ];
436
437 $this->transactionModel = \FluentCart\App\Models\OrderTransaction::query()->create($transactionData);
438
439 // insert the applied coupons
440 $this->insertAppliedCoupons(Arr::get($this->args, 'applied_coupons', []));
441
442 $cartHash = Arr::get($this->args, 'cart_hash', '');
443 if ($cartHash) {
444 $cart = Cart::query()->where('cart_hash', $cartHash)->first();
445 if ($cart) {
446 $cart->order_id = $this->orderModel->id;
447 $cart->customer_id = $this->orderModel->customer_id;
448 $cart->stage = 'intended';
449 $cart->save();
450 $actions = Arr::get($cart->checkout_data, '__after_draft_created_actions__', []);
451 if ($actions) {
452 foreach ($actions as $actionName) {
453 $actionName = (string)$actionName;
454 if (has_action($actionName)) {
455 do_action($actionName, [
456 'order' => $this->orderModel,
457 'cart' => $cart,
458 ]);
459 }
460 }
461 }
462 }
463 }
464
465 // We are almost done!
466 return $this->orderModel;
467 }
468
469
470 private function insertAppliedCoupons($appliedCoupons, $removeOldCoupons = false): void
471 {
472 if ($removeOldCoupons) {
473 $this->orderModel->appliedCoupons()->delete();
474 }
475
476 $couponCodes = array_keys($appliedCoupons);
477
478 if (!empty($couponCodes)) {
479 $coupons = Coupon::query()->whereIn('code', $couponCodes)->get()
480 ->keyBy('code')
481 ->toArray();
482
483 foreach ($coupons as $code => &$coupon) {
484 $coupon['coupon_id'] = $appliedCoupons[$code]['id'];
485 $coupon['amount'] = $appliedCoupons[$code]['discount'];
486 }
487 $this->orderModel->appliedCoupons()->createMany($coupons);
488
489 Coupon::query()
490 ->whereIn('code', $couponCodes)
491 ->increment('use_count', 1);
492 }
493 }
494
495 public function getTransaction()
496 {
497 return $this->transactionModel;
498 }
499
500 public function getOrder()
501 {
502 return $this->orderModel;
503 }
504
505 public function getSubscription()
506 {
507 return $this->subscriptionModel;
508 }
509 private function prepareSubscriptionData()
510 {
511 $subscriptionItems = array_filter($this->formattedIOrderItems, function ($item) {
512 return $item['payment_type'] === 'subscription';
513 });
514
515 $signupFeeItems = array_filter($this->formattedIOrderItems, function ($item) {
516 return $item['payment_type'] === 'signup_fee';
517 });
518
519 if (!$subscriptionItems) {
520 return;
521 }
522
523 if (count($subscriptionItems) > 1) {
524 return;
525 }
526
527 $item = reset($subscriptionItems);
528 $signupFeeItem = reset($signupFeeItems) ?? [];
529
530 $totalSignup = Arr::get($item, 'other_info.signup_fee', 0) - Arr::get($item, 'other_info.signup_discount', 0);
531 $firstPrice = Arr::get($item, 'subtotal') + $totalSignup - Arr::get($item, 'discount_total', 0);
532 $recurringPrice = Arr::get($item, 'subtotal', 0) + Arr::get($item, 'tax_amount', 0);
533
534 if ($firstPrice < $recurringPrice) {
535 Arr::set($item, 'other_info.trial_days', PaymentHelper::getIntervalDays(Arr::get($item, 'other_info.repeat_interval')));
536 Arr::set($item, 'other_info.signup_fee', $firstPrice);
537 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
538 // times stays the full installment count — the simulated trial cycle is installment #1
539 } else if ($firstPrice > $recurringPrice) {
540 Arr::set($item, 'other_info.signup_fee', $firstPrice - $recurringPrice);
541 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
542 Arr::set($item, 'other_info.trial_days', 0);
543 }
544
545 $subscriptionPricing = $this->convertToSubscriptionFormat([
546 'initial_trial_days' => Arr::get($item, 'other_info.trial_days', 0),
547 'repeat_interval' => Arr::get($item, 'other_info.repeat_interval', 'monthly'),
548 'times' => Arr::get($item, 'other_info.times', 0),
549 'recurring_amount' => $item['subtotal'],
550 'signup_fee' => $signupFeeItem ? $signupFeeItem['subtotal'] : 0,
551 'total_discount' => $item['discount_total'] + Arr::get($signupFeeItem, 'discount_total', 0)
552 ]);
553
554 // removable upon discussion
555 $subscriptionItem = [
556 'product_id' => $item['post_id'],
557 'current_payment_method' => Arr::get($this->orderData, 'payment_method'),
558 'object_id' => $item['object_id'],
559 'recurring_tax_total' => 0,
560 'recurring_total' => $subscriptionPricing['recurring_amount'], //use price not line_total to ignore discount
561 'item_name' => $item['post_title'] . ' - ' . $item['title'],
562 'bill_count' => 0,
563 'quantity' => 1,
564 'variation_id' => Arr::get($item, 'object_id', 0),
565 'status' => Status::SUBSCRIPTION_PENDING,
566 'config' => [
567 'currency' => $this->orderData['currency'],
568 'is_trial_days_simulated' => Arr::get($subscriptionPricing, 'is_trial_days_simulated', 'no'),
569 // Snapshot the variant attribute map + variation type from the order
570 // item so the subscription carries the same pa_* set behind its item_name.
571 'item_attributes' => Arr::get($item, 'other_info.item_attributes', []),
572 'variation_type' => Arr::get($item, 'other_info.variation_type', ''),
573 ]
574 ];
575
576 $this->subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
577 }
578
579 /**
580 * @param $inputData
581 * @return array
582 */
583 private function convertToSubscriptionFormat($inputData)
584 {
585
586 /**
587 * Normal Subscription $100/month
588 * {
589 * 'trial_days' => 0,
590 * 'repeat_interval' => 'month',
591 * 'times' => 0, // 0 means unlimited
592 * 'recurring_amount' => 100,
593 * 'signup_fee' => 0
594 * }
595 *
596 * 30 Days Trial $100/month
597 * {
598 * 'trial_days' => 30,
599 * 'repeat_interval' => 'month',
600 * 'times' => 0, // 0 means unlimited
601 * 'recurring_amount' => 100,
602 * }
603 *
604 * $100/month - 30 days Trial with $40 signup fee
605 * {
606 * 'trial_days' => 30,
607 * 'repeat_interval' => 'month',
608 * 'times' => 0, // 0 means unlimited
609 * 'recurring_amount' => 100,
610 * 'signup_fee' => 40
611 * }
612 *
613 * $100 / month with $40 signup fee
614 * {
615 * 'trial_days' => 0,
616 * 'repeat_interval' => 'month',
617 * 'times' => 0, // 0 means unlimited
618 * 'recurring_amount' => 100,
619 * 'signup_fee' => 40
620 * }
621 *
622 * $100 per month but 30% discount on first month
623 * {
624 * 'trial_days' => 30,
625 * 'repeat_interval' => 'month',
626 * 'times' => 0, // 0 means unlimited
627 * 'recurring_amount' => 100,
628 * 'signup_fee' => 70
629 * }
630 *
631 * $100 per month but 50% extra on first month
632 * {
633 * 'trial_days' => 0,
634 * 'repeat_interval' => 'month',
635 * 'times' => 0, // 0 means unlimited
636 * 'recurring_amount' => 100,
637 * 'signup_fee' => 50
638 * }
639 *
640 * $100 per month and 1 month trial and service_fee = $50
641 * {
642 * 'trial_days' => 30,
643 * 'repeat_interval' => 'month',
644 * 'times' => 0, // 0 means unlimited
645 * 'recurring_amount' => 100,
646 * 'signup_fee' => 50
647 * }
648 *
649 *
650 * $100 per month, signup fee $200 - First Month 50% discount
651 *
652 * $first Month = 100 + 200 = 300 - 50% discount = 150
653 * {
654 * 'trial_days' => 30,
655 * 'repeat_interval' => 'month',
656 * 'times' => 0, // 0 means unlimited
657 * 'recurring_amount' => 100,
658 * 'signup_fee' => 150
659 * }
660 *
661 * // prefered when signup_fee > recurring_amount
662 * {
663 * 'trial_days' => 0,
664 * 'repeat_interval' => 'month',
665 * 'times' => 0, // 0 means unlimited
666 * 'recurring_amount' => 100,
667 * 'signup_fee' => 50
668 * }
669 */
670
671
672 // Extract and validate input data
673 $trialDays = (int)($inputData['initial_trial_days'] ?? 0);
674 $repeatInterval = strtolower(trim($inputData['repeat_interval'] ?? 'yearly'));
675 $times = (int)($inputData['times'] ?? 0);
676 $recurringAmount = (int)($inputData['recurring_amount'] ?? 0);
677 $signupFee = (int)($inputData['signup_fee'] ?? 0);
678 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
679
680 // Convert repeat_interval to standard format
681 $intervalMap = Helper::getAvailableSubscriptionIntervalMaps();
682 $standardInterval = Helper::translateIntervalToStandardFormat($repeatInterval);
683
684
685 // Initialize result array
686 $result = [
687 'trial_days' => $trialDays,
688 'repeat_interval' => $standardInterval,
689 'times' => $times,
690 'recurring_amount' => $recurringAmount,
691 ];
692
693
694 // Calculate signup fee logic
695 if ($totalDiscount > 0) {
696
697 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
698
699 if ($firstCycleCost < $recurringAmount) {
700 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);
701
702 $result['trial_days'] = $adjustedTrialDays;
703 $result['is_trial_days_simulated'] = 'yes';
704 $result['signup_fee'] = $firstCycleCost;
705 $result['manage_setup_fee'] = 'yes';
706 // bill_times stays the full installment count. The simulated trial cycle IS the
707 // first installment; gateways derive remaining remote cycles from is_trial_days_simulated.
708 } else if ($firstCycleCost > $recurringAmount) {
709 $result['trial_days'] = 0;
710 $result['signup_fee'] = $firstCycleCost - $recurringAmount;
711 $result['manage_setup_fee'] = 'yes';
712 } else if ($firstCycleCost == $recurringAmount) {
713 $result['trial_days'] = 0;
714 $result['signup_fee'] = 0;
715 $result['manage_setup_fee'] = 'no';
716 }
717
718 } else {
719 $result['signup_fee'] = $signupFee;
720 }
721
722 // inverse the repeat interval to match the expected format
723
724 $result['repeat_interval'] = array_flip($intervalMap)[$result['repeat_interval']] ?? 'yearly';
725
726
727 return [
728 'billing_interval' => $result['repeat_interval'],
729 'bill_times' => $result['times'],
730 'trial_days' => $result['trial_days'],
731 'is_trial_days_simulated' => Arr::get($result, 'is_trial_days_simulated', 'no'),
732 'recurring_amount' => $result['recurring_amount'],
733 'signup_fee' => $result['signup_fee'] ?? 0,
734 ];
735 }
736 }
737