PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 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 All 51 releases
← All changes | app/Helpers/CheckoutProcessor.php +91 -11 1.6.0 → 1.7.1 View file →
@@ -22,8 +22,9 @@
22 22
23 23 // Raw Data
24 24 private $cartItems = [];
25 25 private $args = [];
26 + private $validationError;
26 27
27 28 // Order Related Data
28 29 private $formattedIOrderItems = [];
29 30 private $orderData = [];
@@ -62,14 +63,43 @@
62 63
63 64 private function prepareData()
64 65 {
65 66 $this->prepareOrderItems();
67 + if ($this->validationError) {
68 + return;
69 + }
70 +
66 71 $this->prepareOrderData();
72 + if ($this->validationError) {
73 + return;
74 + }
75 +
67 76 $this->prepareSubscriptionData();
68 77 }
69 78
79 + /**
80 + * (int) on an out-of-range float wraps, and the max(0, ...) clamps downstream turn a
81 + * wrapped amount into a free but payable order. Refuse the checkout instead.
82 + */
83 + private function isSafeAmount($value)
84 + {
85 + return is_numeric($value) && is_finite((float)$value) && abs((float)$value) < (float)PHP_INT_MAX;
86 + }
87 +
88 + private function unsafeAmountError()
89 + {
90 + return new \WP_Error(
91 + 'invalid_order_total',
92 + __('The order total is too large to process. Please reduce the quantity.', 'fluent-cart')
93 + );
94 + }
95 +
70 96 public function createDraftOrder($prevOrder = null)
71 97 {
98 + if ($this->validationError) {
99 + return $this->validationError;
100 + }
101 +
72 102 if ($prevOrder) {
73 103 return $this->getAdjustedOrder($prevOrder);
74 104 }
75 105
@@ -246,8 +276,18 @@
246 276 $cart->user_id = $customer->user_id;
247 277 }
248 278
249 279 $cart->save();
280 +
281 + // Carry the traffic source onto the order while the cart still exists.
282 + // Carts are pruned on a schedule, so this is the last reliable point at
283 + // which the click that produced the sale can still be recovered.
284 + UtmHelper::addUtmToOrder(
285 + $this->orderModel->id,
286 + UtmHelper::resolveUtmData(UtmHelper::getUtmDataOfRequest(), $cart->utm_data),
287 + $cart->cart_hash
288 + );
289 +
250 290 $actions = Arr::get($cart->checkout_data, '__after_draft_created_actions__', []);
251 291 if ($actions) {
252 292 foreach ($actions as $actionName) {
253 293 $actionName = (string)$actionName;
@@ -476,19 +516,25 @@
476 516 ->where('order_id', $this->orderModel->id)
477 517 ->first();
478 518
479 519 if ($existingTransaction) {
520 + $meta = $existingTransaction->meta ?: [];
521 +
480 522 // Retry vs duplicate for gateway idempotency (PaymentInstance::getIdempotencySeed):
481 523 // re-submitting a pending transaction is a duplicate (keep attempt -> gateway
482 524 // dedupes); re-submitting a FAILED one is a retry (bump attempt -> fresh seed,
483 525 // never answered with the failed attempt's cached gateway response).
484 - $attempt = (int) Arr::get($existingTransaction->meta ?: [], 'payment_attempt', 0);
526 + $attempt = (int) Arr::get($meta, 'payment_attempt', 0);
485 527 if ($existingTransaction->status === Status::PAYMENT_FAILED) {
486 528 $attempt++;
487 529 }
530 +
531 + // The gateway object prepared last time (a Paddle transaction, a PayPal
532 + // order) is kept so the gateway can reuse it instead of creating another.
488 533 if ($attempt) {
489 - $transactionData['meta'] = ['payment_attempt' => $attempt];
534 + $meta['payment_attempt'] = $attempt;
490 535 }
536 + $transactionData['meta'] = $meta;
491 537
492 538 $existingTransaction->fill($transactionData);
493 539 $existingTransaction->save();
494 540 $this->transactionModel = $existingTransaction;
@@ -606,9 +652,16 @@
606 652
607 653 $discountTotal = (int)Arr::get($cartItem, 'manual_discount', 0) + (int)Arr::get($cartItem, 'coupon_discount', 0);
608 654 $shippingCharge = (int)Arr::get($cartItem, 'shipping_charge', 0);
609 655
610 - $subtotal = (int) Arr::get($cartItem, 'subtotal', $unitPrice * $quantity);
656 + $rawSubtotal = Arr::get($cartItem, 'subtotal', $unitPrice * $quantity);
657 + if (!$this->isSafeAmount($rawSubtotal)) {
658 + $this->validationError = $this->unsafeAmountError();
659 +
660 + return;
661 + }
662 +
663 + $subtotal = (int) $rawSubtotal;
611 664 $args = Arr::get($cartItem, 'other_info', []);
612 665 $paymentType = Arr::get($args, 'payment_type', 'default');
613 666
614 667 $postTitle = Arr::get($cartItem, 'product_title', '');
@@ -915,9 +968,9 @@
915 968 'line_meta' => Arr::get($item, 'line_meta', []),
916 969 'signup_fee' => $signupFee,
917 970 'signup_fee_tax' => $signupFeeTax,
918 971 'first_iteration_tax' => $firstIterationTax,
919 - 'is_recurring_coupon' => Arr::get($item, 'is_recurring_coupon', 'no'),
972 + 'recurring_discount' => $recurringDiscountAmount,
920 973 'total_discount' => $discountTotal
921 974 ]);
922 975
923 976 // removable upon discussion
@@ -941,13 +994,8 @@
941 994 'variation_type' => Arr::get($item, 'other_info.variation_type', '')
942 995 ]
943 996 ];
944 997
945 - // if recurring coupon is applied, we need to subtract the total discount from the recurring total
946 - if (Arr::get($item, 'is_recurring_coupon', 'no') === 'yes') {
947 - $subscriptionItem['recurring_total'] -= $discountTotal;
948 - }
949 -
950 998 $subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
951 999 $paymentMethod = Arr::get($this->orderData, 'payment_method', '');
952 1000
953 1001 $collectionMethod = apply_filters('fluent_cart/subscription_collection_method_' . $paymentMethod, $this->determineCollectionMethod());
@@ -1093,8 +1141,36 @@
1093 1141 + $estimatedTaxTotal
1094 1142 + $estimatedShippingTax;
1095 1143
1096 1144 $orderData['total_amount'] = $totalAmount > 0 ? $totalAmount : 0;
1145 +
1146 + /**
1147 + * Filter the prepared order data before it is used for order creation.
1148 + *
1149 + * This runs after FluentCart calculates totals, so plugins can adjust
1150 + * currency, rate, totals, config, mode, or any other order field before
1151 + * the order model, transaction, and subscription are derived from it.
1152 + *
1153 + * @param array $orderData Prepared order data array.
1154 + * @param array $context {
1155 + * Additional context for the filter.
1156 + *
1157 + * @type array $items Formatted order items with prices and quantities.
1158 + * @type array $args Checkout arguments: customer data, payment method,
1159 + * shipping, tax, coupons, fees, and IP data.
1160 + * }
1161 + */
1162 + $orderData = apply_filters('fluent_cart/checkout/order_data', $orderData, [
1163 + 'items' => $this->formattedIOrderItems,
1164 + 'args' => $this->args,
1165 + ]);
1166 +
1167 + if (!$this->isSafeAmount(Arr::get($orderData, 'total_amount', 0))) {
1168 + $this->validationError = $this->unsafeAmountError();
1169 +
1170 + return;
1171 + }
1172 +
1097 1173 $this->orderData = $orderData;
1098 1174 }
1099 1175
1100 1176 private function syncFeeItems()
@@ -1219,8 +1295,9 @@
1219 1295 $signupFee = (int)($inputData['signup_fee'] ?? 0);
1220 1296 $signupFeeTax = (int)($inputData['signup_fee_tax'] ?? 0);
1221 1297 $firstIterationTax = (int)($inputData['first_iteration_tax'] ?? 0);
1222 1298 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
1299 + $recurringDiscount = (int)($inputData['recurring_discount'] ?? 0);
1223 1300
1224 1301 // Determine if THIS subscription item is tax-inclusive (for behavior=3 mixed carts)
1225 1302 $taxBehavior = (int) Arr::get($inputData, 'tax_behavior', 0);
1226 1303 $itemInclusive = (bool) Arr::get($inputData, 'line_meta.tax_config.inclusive', false);
@@ -1260,10 +1337,13 @@
1260 1337 }
1261 1338 } else {
1262 1339 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
1263 1340
1264 - if (Arr::get($inputData, 'is_recurring_coupon', 'no') === 'yes') {
1265 - $recurringAmount -= $totalDiscount; // as now discount applied on recurring amount
1341 + // A recurring coupon discounts every cycle, so the per-cycle price itself
1342 + // is lower — the first cycle is not cheaper than the ones after it and
1343 + // must not be expressed as a trial.
1344 + if ($recurringDiscount > 0) {
1345 + $recurringAmount -= $recurringDiscount;
1266 1346 }
1267 1347
1268 1348 if ($firstCycleCost < $recurringAmount) {
1269 1349 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);