PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
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 trunk All 48 releases
fluent-cart / app / Helpers / AdminOrderProcessor.php

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

739 lines 31.0 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 'source' => 'admin',
280 ],
281 ];
282
283 $totalAmount = $orderData['subtotal'] - $orderData['coupon_discount_total'] - $orderData['manual_discount_total'] + $orderData['shipping_total'] + $orderData['tax_total'];
284 $orderData['total_amount'] = $totalAmount > 0 ? $totalAmount : 0;
285 $this->orderData = $orderData;
286 }
287
288
289 public function createDraftOrder($prevOrder = null)
290 {
291 $customerId = Arr::get($this->args, 'customer_id', '');
292 if (!$customerId) {
293 return new \WP_Error('customer_id_missing', __('Customer ID is required to create a draft order.', 'fluent-cart'));
294 }
295
296 $orderData = $this->orderData;
297 $orderData['customer_id'] = $customerId;
298 $this->orderModel = \FluentCart\App\Models\Order::query()->create($orderData);
299
300 if (!$this->orderModel) {
301 return new \WP_Error('order_creation_failed', __('Failed to create order.', 'fluent-cart'));
302 }
303
304 // Let's create the order items
305 $normalOrderItems = array_filter($this->formattedIOrderItems, function ($item) {
306 return $item['payment_type'] != 'signup_fee';
307 });
308
309
310 foreach ($normalOrderItems as $orderItem) {
311 $orderItem['order_id'] = $this->orderModel->id;
312 $orderItem['line_total'] = $orderItem['subtotal'] - $orderItem['discount_total'];
313 $additionalItems = [];
314 $bundleItems = [];
315 if ($orderItem['payment_type'] == 'subscription') {
316 // this is a subscription type. We may have additional_items
317 $additionalItems = Arr::get($orderItem, 'additional_items', []);
318 unset($orderItem['additional_items']);
319 }
320
321 if (Arr::get($orderItem, 'other_info.is_bundle_product', 'no') == 'yes') {
322 $bundleItems = Arr::get($orderItem, 'bundle_items', []);
323 unset($orderItem['bundle_items']);
324 }
325
326 if (!empty($orderItem['coupon_discount'])) {
327 $lineMeta = Arr::get($orderItem, 'line_meta', []);
328 $lineMeta['coupon_discount'] = (int) $orderItem['coupon_discount'];
329 $orderItem['line_meta'] = $lineMeta;
330 }
331
332 $createdItem = OrderItem::query()->create($orderItem);
333
334 if ($additionalItems) {
335 $additionalItemIds = [];
336 foreach ($additionalItems as $additionalItem) {
337 $additionalItem['order_id'] = $this->orderModel->id;
338 $additionalItem['line_total'] = $additionalItem['subtotal'] - $additionalItem['discount_total'];
339 $mata = Arr::get($additionalItem, 'line_meta', []);
340 $mata['parent_item_id'] = $createdItem->id;
341 if (!empty($additionalItem['coupon_discount'])) {
342 $mata['coupon_discount'] = (int) $additionalItem['coupon_discount'];
343 }
344 $additionalItem['line_meta'] = $mata;
345 OrderItem::query()->create($additionalItem);
346 }
347
348 $createdItem->fill([
349 'line_meta' => array_merge(
350 $createdItem->line_meta,
351 [
352 'additional_item_ids' => $additionalItemIds
353 ]
354 )
355 ])->save();
356 }
357
358 if ($bundleItems) {
359 $bundleItemIds = [];
360 foreach ($bundleItems as $bundleItem) {
361 $bundleItem['order_id'] = $this->orderModel->id;
362 $bundleItem['line_total'] = Arr::get($bundleItem, 'subtotal', 0) - Arr::get($bundleItem, 'discount_total', 0);
363 $bundleItem['payment_type'] = 'bundle';
364 $meta = Arr::get($bundleItem, 'line_meta', []);
365 $meta['bundle_parent_item_id'] = $createdItem->id;
366 if (!empty($bundleItem['coupon_discount'])) {
367 $meta['coupon_discount'] = (int) $bundleItem['coupon_discount'];
368 }
369 $bundleItem['line_meta'] = $meta;
370 $bundleItem = OrderItem::query()->create($bundleItem);
371 $bundleItemIds[] = $bundleItem->id;
372 }
373
374 $createdItem->fill([
375 'line_meta' => array_merge(
376 $createdItem->line_meta,
377 [
378 'bundle_item_ids' => $bundleItemIds
379 ]
380 )
381 ])->save();
382 }
383 }
384
385 // Let's create the subscription if exists
386 if ($this->subscriptionData) {
387 $subscriptionData = $this->subscriptionData;
388 $subscriptionData['customer_id'] = $customerId;
389 $subscriptionData['parent_order_id'] = $this->orderModel->id;
390 $this->subscriptionModel = Subscription::query()->create($subscriptionData);
391
392 // bill_count counting can't tell "billed cycle" from "something else
393 // charged alongside it." Must be decided at creation: payment-method switching
394 // also sets is_trial_days_simulated, so the flag alone can't be trusted at runtime.
395 $isSimulated = Arr::get($subscriptionData, 'config.is_trial_days_simulated', 'no') === 'yes';
396 $trialDays = (int)Arr::get($subscriptionData, 'trial_days', 0);
397 $billTimes = (int)$this->subscriptionModel->bill_times;
398 $orderTotal = (int)$this->orderModel->total_amount;
399
400 // Simulated trial, $0 first cycle: consumes a cycle but produces no
401 // total > 0 transaction — add billed_cycles_offset so it still counts.
402 if ($isSimulated
403 && $billTimes > 0
404 && (int)$this->subscriptionModel->signup_fee <= 0
405 && !$orderTotal
406 ) {
407 $this->subscriptionModel->updateMeta('billed_cycles_offset', 1);
408 }
409
410 // Real trial with a signup fee: the initial charge is the signup fee only,
411 // but it IS a total > 0 transaction linked to the subscription — mark
412 // billed_cycles_deduction so it does NOT count as a cycle.
413 if (!$isSimulated
414 && $trialDays > 0
415 && $billTimes > 0
416 && $orderTotal > 0
417 ) {
418 $this->subscriptionModel->updateMeta('billed_cycles_deduction', 1);
419 }
420 }
421
422 // Let's create the transaction
423 $transactionData = [
424 'order_id' => $this->orderModel->id,
425 'order_type' => $this->orderModel->type,
426 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
427 'subscription_id' => $this->subscriptionModel ? $this->subscriptionModel->id : NULL,
428 'payment_method' => $this->orderModel->payment_method,
429 'payment_mode' => $this->orderModel->mode,
430 'payment_method_type' => '',
431 'status' => Status::PAYMENT_PENDING,
432 'currency' => $this->orderModel->currency,
433 'total' => $this->orderModel->total_amount,
434 'rate' => 1,
435 'meta' => [],
436 ];
437
438 $this->transactionModel = \FluentCart\App\Models\OrderTransaction::query()->create($transactionData);
439
440 // insert the applied coupons
441 $this->insertAppliedCoupons(Arr::get($this->args, 'applied_coupons', []));
442
443 $cartHash = Arr::get($this->args, 'cart_hash', '');
444 if ($cartHash) {
445 $cart = Cart::query()->where('cart_hash', $cartHash)->first();
446 if ($cart) {
447 $cart->order_id = $this->orderModel->id;
448 $cart->customer_id = $this->orderModel->customer_id;
449 $cart->stage = 'intended';
450 $cart->save();
451 $actions = Arr::get($cart->checkout_data, '__after_draft_created_actions__', []);
452 if ($actions) {
453 foreach ($actions as $actionName) {
454 $actionName = (string)$actionName;
455 if (has_action($actionName)) {
456 do_action($actionName, [
457 'order' => $this->orderModel,
458 'cart' => $cart,
459 ]);
460 }
461 }
462 }
463 }
464 }
465
466 // We are almost done!
467 return $this->orderModel;
468 }
469
470
471 private function insertAppliedCoupons($appliedCoupons, $removeOldCoupons = false): void
472 {
473 if ($removeOldCoupons) {
474 $this->orderModel->appliedCoupons()->delete();
475 }
476
477 $couponCodes = array_keys($appliedCoupons);
478
479 if (!empty($couponCodes)) {
480 $coupons = Coupon::query()->whereIn('code', $couponCodes)->get()
481 ->keyBy('code')
482 ->toArray();
483
484 foreach ($coupons as $code => &$coupon) {
485 $coupon['coupon_id'] = $appliedCoupons[$code]['id'];
486 $coupon['amount'] = $appliedCoupons[$code]['discount'];
487 }
488 $this->orderModel->appliedCoupons()->createMany($coupons);
489
490 Coupon::query()
491 ->whereIn('code', $couponCodes)
492 ->increment('use_count', 1);
493 }
494 }
495
496 public function getTransaction()
497 {
498 return $this->transactionModel;
499 }
500
501 public function getOrder()
502 {
503 return $this->orderModel;
504 }
505
506 public function getSubscription()
507 {
508 return $this->subscriptionModel;
509 }
510 private function prepareSubscriptionData()
511 {
512 $subscriptionItems = array_filter($this->formattedIOrderItems, function ($item) {
513 return $item['payment_type'] === 'subscription';
514 });
515
516 $signupFeeItems = array_filter($this->formattedIOrderItems, function ($item) {
517 return $item['payment_type'] === 'signup_fee';
518 });
519
520 if (!$subscriptionItems) {
521 return;
522 }
523
524 if (count($subscriptionItems) > 1) {
525 return;
526 }
527
528 $item = reset($subscriptionItems);
529 $signupFeeItem = reset($signupFeeItems) ?? [];
530
531 $totalSignup = Arr::get($item, 'other_info.signup_fee', 0) - Arr::get($item, 'other_info.signup_discount', 0);
532 $firstPrice = Arr::get($item, 'subtotal') + $totalSignup - Arr::get($item, 'discount_total', 0);
533 $recurringPrice = Arr::get($item, 'subtotal', 0) + Arr::get($item, 'tax_amount', 0);
534
535 if ($firstPrice < $recurringPrice) {
536 Arr::set($item, 'other_info.trial_days', PaymentHelper::getIntervalDays(Arr::get($item, 'other_info.repeat_interval')));
537 Arr::set($item, 'other_info.signup_fee', $firstPrice);
538 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
539 // times stays the full installment count — the simulated trial cycle is installment #1
540 } else if ($firstPrice > $recurringPrice) {
541 Arr::set($item, 'other_info.signup_fee', $firstPrice - $recurringPrice);
542 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
543 Arr::set($item, 'other_info.trial_days', 0);
544 }
545
546 $subscriptionPricing = $this->convertToSubscriptionFormat([
547 'initial_trial_days' => Arr::get($item, 'other_info.trial_days', 0),
548 'repeat_interval' => Arr::get($item, 'other_info.repeat_interval', 'monthly'),
549 'times' => Arr::get($item, 'other_info.times', 0),
550 'recurring_amount' => $item['subtotal'],
551 'signup_fee' => $signupFeeItem ? $signupFeeItem['subtotal'] : 0,
552 'total_discount' => $item['discount_total'] + Arr::get($signupFeeItem, 'discount_total', 0)
553 ]);
554
555 // removable upon discussion
556 $subscriptionItem = [
557 'product_id' => $item['post_id'],
558 'current_payment_method' => Arr::get($this->orderData, 'payment_method'),
559 'object_id' => $item['object_id'],
560 'recurring_tax_total' => 0,
561 'recurring_total' => $subscriptionPricing['recurring_amount'], //use price not line_total to ignore discount
562 'item_name' => $item['post_title'] . ' - ' . $item['title'],
563 'bill_count' => 0,
564 'quantity' => 1,
565 'variation_id' => Arr::get($item, 'object_id', 0),
566 'status' => Status::SUBSCRIPTION_PENDING,
567 'collection_method' => Status::SUBSCRIPTION_METHOD_MANUAL,
568 'config' => [
569 'currency' => $this->orderData['currency'],
570 'is_trial_days_simulated' => Arr::get($subscriptionPricing, 'is_trial_days_simulated', 'no'),
571 // Snapshot the variant attribute map + variation type from the order
572 // item so the subscription carries the same pa_* set behind its item_name.
573 'item_attributes' => Arr::get($item, 'other_info.item_attributes', []),
574 'variation_type' => Arr::get($item, 'other_info.variation_type', ''),
575 ]
576 ];
577
578 $this->subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
579 }
580
581 /**
582 * @param $inputData
583 * @return array
584 */
585 private function convertToSubscriptionFormat($inputData)
586 {
587
588 /**
589 * Normal Subscription $100/month
590 * {
591 * 'trial_days' => 0,
592 * 'repeat_interval' => 'month',
593 * 'times' => 0, // 0 means unlimited
594 * 'recurring_amount' => 100,
595 * 'signup_fee' => 0
596 * }
597 *
598 * 30 Days Trial $100/month
599 * {
600 * 'trial_days' => 30,
601 * 'repeat_interval' => 'month',
602 * 'times' => 0, // 0 means unlimited
603 * 'recurring_amount' => 100,
604 * }
605 *
606 * $100/month - 30 days Trial with $40 signup fee
607 * {
608 * 'trial_days' => 30,
609 * 'repeat_interval' => 'month',
610 * 'times' => 0, // 0 means unlimited
611 * 'recurring_amount' => 100,
612 * 'signup_fee' => 40
613 * }
614 *
615 * $100 / month with $40 signup fee
616 * {
617 * 'trial_days' => 0,
618 * 'repeat_interval' => 'month',
619 * 'times' => 0, // 0 means unlimited
620 * 'recurring_amount' => 100,
621 * 'signup_fee' => 40
622 * }
623 *
624 * $100 per month but 30% discount on first month
625 * {
626 * 'trial_days' => 30,
627 * 'repeat_interval' => 'month',
628 * 'times' => 0, // 0 means unlimited
629 * 'recurring_amount' => 100,
630 * 'signup_fee' => 70
631 * }
632 *
633 * $100 per month but 50% extra on first month
634 * {
635 * 'trial_days' => 0,
636 * 'repeat_interval' => 'month',
637 * 'times' => 0, // 0 means unlimited
638 * 'recurring_amount' => 100,
639 * 'signup_fee' => 50
640 * }
641 *
642 * $100 per month and 1 month trial and service_fee = $50
643 * {
644 * 'trial_days' => 30,
645 * 'repeat_interval' => 'month',
646 * 'times' => 0, // 0 means unlimited
647 * 'recurring_amount' => 100,
648 * 'signup_fee' => 50
649 * }
650 *
651 *
652 * $100 per month, signup fee $200 - First Month 50% discount
653 *
654 * $first Month = 100 + 200 = 300 - 50% discount = 150
655 * {
656 * 'trial_days' => 30,
657 * 'repeat_interval' => 'month',
658 * 'times' => 0, // 0 means unlimited
659 * 'recurring_amount' => 100,
660 * 'signup_fee' => 150
661 * }
662 *
663 * // prefered when signup_fee > recurring_amount
664 * {
665 * 'trial_days' => 0,
666 * 'repeat_interval' => 'month',
667 * 'times' => 0, // 0 means unlimited
668 * 'recurring_amount' => 100,
669 * 'signup_fee' => 50
670 * }
671 */
672
673
674 // Extract and validate input data
675 $trialDays = (int)($inputData['initial_trial_days'] ?? 0);
676 $repeatInterval = strtolower(trim($inputData['repeat_interval'] ?? 'yearly'));
677 $times = (int)($inputData['times'] ?? 0);
678 $recurringAmount = (int)($inputData['recurring_amount'] ?? 0);
679 $signupFee = (int)($inputData['signup_fee'] ?? 0);
680 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
681
682 // Convert repeat_interval to standard format
683 $intervalMap = Helper::getAvailableSubscriptionIntervalMaps();
684 $standardInterval = Helper::translateIntervalToStandardFormat($repeatInterval);
685
686
687 // Initialize result array
688 $result = [
689 'trial_days' => $trialDays,
690 'repeat_interval' => $standardInterval,
691 'times' => $times,
692 'recurring_amount' => $recurringAmount,
693 ];
694
695
696 // Calculate signup fee logic
697 if ($totalDiscount > 0) {
698
699 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
700
701 if ($firstCycleCost < $recurringAmount) {
702 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);
703
704 $result['trial_days'] = $adjustedTrialDays;
705 $result['is_trial_days_simulated'] = 'yes';
706 $result['signup_fee'] = $firstCycleCost;
707 $result['manage_setup_fee'] = 'yes';
708 // bill_times stays the full installment count. The simulated trial cycle IS the
709 // first installment; gateways derive remaining remote cycles from is_trial_days_simulated.
710 } else if ($firstCycleCost > $recurringAmount) {
711 $result['trial_days'] = 0;
712 $result['signup_fee'] = $firstCycleCost - $recurringAmount;
713 $result['manage_setup_fee'] = 'yes';
714 } else if ($firstCycleCost == $recurringAmount) {
715 $result['trial_days'] = 0;
716 $result['signup_fee'] = 0;
717 $result['manage_setup_fee'] = 'no';
718 }
719
720 } else {
721 $result['signup_fee'] = $signupFee;
722 }
723
724 // inverse the repeat interval to match the expected format
725
726 $result['repeat_interval'] = array_flip($intervalMap)[$result['repeat_interval']] ?? 'yearly';
727
728
729 return [
730 'billing_interval' => $result['repeat_interval'],
731 'bill_times' => $result['times'],
732 'trial_days' => $result['trial_days'],
733 'is_trial_days_simulated' => Arr::get($result, 'is_trial_days_simulated', 'no'),
734 'recurring_amount' => $result['recurring_amount'],
735 'signup_fee' => $result['signup_fee'] ?? 0,
736 ];
737 }
738 }
739