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

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

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