PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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
← All changes | app/Helpers/AdminOrderProcessor.php +105 -3 1.3.27 → 1.6.5 View file →
@@ -86,8 +86,9 @@
86 86 $postTitle = $product->post_title;
87 87 }
88 88 }
89 89
90 + $variation = null;
90 91 if (!$variationTitle) {
91 92 $variation = ProductVariation::query()->find(Arr::get($checkoutItem, 'object_id', 0));
92 93 if ($variation) {
93 94 $variationTitle = $variation->variation_title;
@@ -93,9 +94,33 @@
93 94 $variationTitle = $variation->variation_title;
94 95 }
95 96 }
96 97
98 + if (!$variation) {
99 + $variation = ProductVariation::query()->find(Arr::get($checkoutItem, 'object_id', 0));
100 + }
97 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 +
98 123 $item = [
99 124 'payment_type' => $paymentType,
100 125 'post_id' => Arr::get($checkoutItem, 'post_id'),
101 126 'object_id' => Arr::get($checkoutItem, 'object_id'),
@@ -100,9 +125,9 @@
100 125 'post_id' => Arr::get($checkoutItem, 'post_id'),
101 126 'object_id' => Arr::get($checkoutItem, 'object_id'),
102 127 'post_title' => $postTitle,
103 128 'title' => $variationTitle,
104 - 'fulfillment_type' => Arr::get($checkoutItem, 'fulfillment_type', 'digital'),
129 + 'fulfillment_type' => $fulfillmentType,
105 130 'quantity' => $quantity,
106 131 'cost' => (int)Arr::get($checkoutItem, 'cost', 0),
107 132 'unit_price' => $unitPrice,
108 133 'subtotal' => $subtotal,
@@ -108,8 +133,9 @@
108 133 'subtotal' => $subtotal,
109 134 'tax_amount' => $tax,
110 135 'shipping_charge' => $shippingCharge,
111 136 'discount_total' => $discountTotal,
137 + 'coupon_discount' => (int)Arr::get($checkoutItem, 'discount_total', 0),
112 138 'other_info' => $args,
113 139 'line_meta' => []
114 140 ];
115 141
@@ -118,13 +144,18 @@
118 144 // We have a signup fee for subscription
119 145 $singupFeeAmount = (int)Arr::get($checkoutItem, 'other_info.signup_fee', 0);
120 146
121 147 $childDiscontTotal = 0;
148 + $childCouponDiscount = 0;
122 149 $signupFeeSubtotal = $singupFeeAmount * $quantity;
150 + $couponDiscount = $item['coupon_discount'];
151 +
123 152 if ($discountTotal) {
124 153 // Distribute discount with signup fee and item
125 154 $childDiscontTotal = (int)($discountTotal / ($subtotal + $signupFeeSubtotal) * $signupFeeSubtotal);
126 155 $discountTotal -= $childDiscontTotal;
156 + $childCouponDiscount = (int) round($couponDiscount * $signupFeeSubtotal / ($subtotal + $signupFeeSubtotal));
157 + $couponDiscount -= $childCouponDiscount;
127 158 }
128 159
129 160 $childItem = [
130 161 'payment_type' => 'signup_fee',
@@ -139,12 +170,14 @@
139 170 'subtotal' => $signupFeeSubtotal,
140 171 'tax_amount' => 0,
141 172 'shipping_charge' => 0,
142 173 'discount_total' => $childDiscontTotal,
174 + 'coupon_discount' => $childCouponDiscount,
143 175 'line_meta' => []
144 176 ];
145 177
146 178 $item['discount_total'] = $discountTotal;
179 + $item['coupon_discount'] = $couponDiscount;
147 180 $item['additional_items'] = [$childItem];
148 181
149 182 Arr::set($item, 'other_info.signup_fee', $singupFeeAmount);
150 183 Arr::set($item, 'other_info.signup_discount', $childDiscontTotal);
@@ -242,13 +275,35 @@
242 275 'note' => Arr::get($this->args, 'note', ''),
243 276 'ip_address' => Arr::get($this->args, 'ip_address', ''),
244 277 'config' => [
245 278 'user_tz' => Arr::get($this->args, 'user_tz', ''),
279 + 'source' => 'admin',
246 280 ],
247 281 ];
248 282
249 283 $totalAmount = $orderData['subtotal'] - $orderData['coupon_discount_total'] - $orderData['manual_discount_total'] + $orderData['shipping_total'] + $orderData['tax_total'];
250 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 +
251 306 $this->orderData = $orderData;
252 307 }
253 308
254 309
@@ -288,8 +343,14 @@
288 343 $bundleItems = Arr::get($orderItem, 'bundle_items', []);
289 344 unset($orderItem['bundle_items']);
290 345 }
291 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 +
292 353 $createdItem = OrderItem::query()->create($orderItem);
293 354
294 355 if ($additionalItems) {
295 356 $additionalItemIds = [];
@@ -297,8 +358,11 @@
297 358 $additionalItem['order_id'] = $this->orderModel->id;
298 359 $additionalItem['line_total'] = $additionalItem['subtotal'] - $additionalItem['discount_total'];
299 360 $mata = Arr::get($additionalItem, 'line_meta', []);
300 361 $mata['parent_item_id'] = $createdItem->id;
362 + if (!empty($additionalItem['coupon_discount'])) {
363 + $mata['coupon_discount'] = (int) $additionalItem['coupon_discount'];
364 + }
301 365 $additionalItem['line_meta'] = $mata;
302 366 OrderItem::query()->create($additionalItem);
303 367 }
304 368
@@ -319,8 +383,11 @@
319 383 $bundleItem['line_total'] = Arr::get($bundleItem, 'subtotal', 0) - Arr::get($bundleItem, 'discount_total', 0);
320 384 $bundleItem['payment_type'] = 'bundle';
321 385 $meta = Arr::get($bundleItem, 'line_meta', []);
322 386 $meta['bundle_parent_item_id'] = $createdItem->id;
387 + if (!empty($bundleItem['coupon_discount'])) {
388 + $meta['coupon_discount'] = (int) $bundleItem['coupon_discount'];
389 + }
323 390 $bundleItem['line_meta'] = $meta;
324 391 $bundleItem = OrderItem::query()->create($bundleItem);
325 392 $bundleItemIds[] = $bundleItem->id;
326 393 }
@@ -341,8 +408,37 @@
341 408 $subscriptionData = $this->subscriptionData;
342 409 $subscriptionData['customer_id'] = $customerId;
343 410 $subscriptionData['parent_order_id'] = $this->orderModel->id;
344 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 + }
345 441 }
346 442
347 443 // Let's create the transaction
348 444 $transactionData = [
@@ -460,9 +556,9 @@
460 556 if ($firstPrice < $recurringPrice) {
461 557 Arr::set($item, 'other_info.trial_days', PaymentHelper::getIntervalDays(Arr::get($item, 'other_info.repeat_interval')));
462 558 Arr::set($item, 'other_info.signup_fee', $firstPrice);
463 559 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
464 - Arr::set($item, 'other_info.times', Arr::get($item, 'other_info.times', 0) > 1 ? Arr::get($item, 'other_info.times', 0) - 1 : 0);
560 + // times stays the full installment count — the simulated trial cycle is installment #1
465 561 } else if ($firstPrice > $recurringPrice) {
466 562 Arr::set($item, 'other_info.signup_fee', $firstPrice - $recurringPrice);
467 563 Arr::set($item, 'other_info.manage_setup_fee', 'yes');
468 564 Arr::set($item, 'other_info.trial_days', 0);
@@ -488,11 +584,16 @@
488 584 'bill_count' => 0,
489 585 'quantity' => 1,
490 586 'variation_id' => Arr::get($item, 'object_id', 0),
491 587 'status' => Status::SUBSCRIPTION_PENDING,
588 + 'collection_method' => Status::SUBSCRIPTION_METHOD_MANUAL,
492 589 'config' => [
493 590 'currency' => $this->orderData['currency'],
494 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', ''),
495 596 ]
496 597 ];
497 598
498 599 $this->subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
@@ -624,9 +725,10 @@
624 725 $result['trial_days'] = $adjustedTrialDays;
625 726 $result['is_trial_days_simulated'] = 'yes';
626 727 $result['signup_fee'] = $firstCycleCost;
627 728 $result['manage_setup_fee'] = 'yes';
628 - $result['times'] = $times > 0 ? $times - 1 : 0;
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.
629 731 } else if ($firstCycleCost > $recurringAmount) {
630 732 $result['trial_days'] = 0;
631 733 $result['signup_fee'] = $firstCycleCost - $recurringAmount;
632 734 $result['manage_setup_fee'] = 'yes';