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 +81 -11 1.6.3 → 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
@@ -486,19 +516,25 @@
486 516 ->where('order_id', $this->orderModel->id)
487 517 ->first();
488 518
489 519 if ($existingTransaction) {
520 + $meta = $existingTransaction->meta ?: [];
521 +
490 522 // Retry vs duplicate for gateway idempotency (PaymentInstance::getIdempotencySeed):
491 523 // re-submitting a pending transaction is a duplicate (keep attempt -> gateway
492 524 // dedupes); re-submitting a FAILED one is a retry (bump attempt -> fresh seed,
493 525 // never answered with the failed attempt's cached gateway response).
494 - $attempt = (int) Arr::get($existingTransaction->meta ?: [], 'payment_attempt', 0);
526 + $attempt = (int) Arr::get($meta, 'payment_attempt', 0);
495 527 if ($existingTransaction->status === Status::PAYMENT_FAILED) {
496 528 $attempt++;
497 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.
498 533 if ($attempt) {
499 - $transactionData['meta'] = ['payment_attempt' => $attempt];
534 + $meta['payment_attempt'] = $attempt;
500 535 }
536 + $transactionData['meta'] = $meta;
501 537
502 538 $existingTransaction->fill($transactionData);
503 539 $existingTransaction->save();
504 540 $this->transactionModel = $existingTransaction;
@@ -616,9 +652,16 @@
616 652
617 653 $discountTotal = (int)Arr::get($cartItem, 'manual_discount', 0) + (int)Arr::get($cartItem, 'coupon_discount', 0);
618 654 $shippingCharge = (int)Arr::get($cartItem, 'shipping_charge', 0);
619 655
620 - $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;
621 664 $args = Arr::get($cartItem, 'other_info', []);
622 665 $paymentType = Arr::get($args, 'payment_type', 'default');
623 666
624 667 $postTitle = Arr::get($cartItem, 'product_title', '');
@@ -925,9 +968,9 @@
925 968 'line_meta' => Arr::get($item, 'line_meta', []),
926 969 'signup_fee' => $signupFee,
927 970 'signup_fee_tax' => $signupFeeTax,
928 971 'first_iteration_tax' => $firstIterationTax,
929 - 'is_recurring_coupon' => Arr::get($item, 'is_recurring_coupon', 'no'),
972 + 'recurring_discount' => $recurringDiscountAmount,
930 973 'total_discount' => $discountTotal
931 974 ]);
932 975
933 976 // removable upon discussion
@@ -951,13 +994,8 @@
951 994 'variation_type' => Arr::get($item, 'other_info.variation_type', '')
952 995 ]
953 996 ];
954 997
955 - // if recurring coupon is applied, we need to subtract the total discount from the recurring total
956 - if (Arr::get($item, 'is_recurring_coupon', 'no') === 'yes') {
957 - $subscriptionItem['recurring_total'] -= $discountTotal;
958 - }
959 -
960 998 $subscriptionData = wp_parse_args($subscriptionPricing, $subscriptionItem);
961 999 $paymentMethod = Arr::get($this->orderData, 'payment_method', '');
962 1000
963 1001 $collectionMethod = apply_filters('fluent_cart/subscription_collection_method_' . $paymentMethod, $this->determineCollectionMethod());
@@ -1103,8 +1141,36 @@
1103 1141 + $estimatedTaxTotal
1104 1142 + $estimatedShippingTax;
1105 1143
1106 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 +
1107 1173 $this->orderData = $orderData;
1108 1174 }
1109 1175
1110 1176 private function syncFeeItems()
@@ -1229,8 +1295,9 @@
1229 1295 $signupFee = (int)($inputData['signup_fee'] ?? 0);
1230 1296 $signupFeeTax = (int)($inputData['signup_fee_tax'] ?? 0);
1231 1297 $firstIterationTax = (int)($inputData['first_iteration_tax'] ?? 0);
1232 1298 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
1299 + $recurringDiscount = (int)($inputData['recurring_discount'] ?? 0);
1233 1300
1234 1301 // Determine if THIS subscription item is tax-inclusive (for behavior=3 mixed carts)
1235 1302 $taxBehavior = (int) Arr::get($inputData, 'tax_behavior', 0);
1236 1303 $itemInclusive = (bool) Arr::get($inputData, 'line_meta.tax_config.inclusive', false);
@@ -1270,10 +1337,13 @@
1270 1337 }
1271 1338 } else {
1272 1339 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
1273 1340
1274 - if (Arr::get($inputData, 'is_recurring_coupon', 'no') === 'yes') {
1275 - $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;
1276 1346 }
1277 1347
1278 1348 if ($firstCycleCost < $recurringAmount) {
1279 1349 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);