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 +59 -11 1.6.4 → 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());
@@ -1125,8 +1163,14 @@
1125 1163 'items' => $this->formattedIOrderItems,
1126 1164 'args' => $this->args,
1127 1165 ]);
1128 1166
1167 + if (!$this->isSafeAmount(Arr::get($orderData, 'total_amount', 0))) {
1168 + $this->validationError = $this->unsafeAmountError();
1169 +
1170 + return;
1171 + }
1172 +
1129 1173 $this->orderData = $orderData;
1130 1174 }
1131 1175
1132 1176 private function syncFeeItems()
@@ -1251,8 +1295,9 @@
1251 1295 $signupFee = (int)($inputData['signup_fee'] ?? 0);
1252 1296 $signupFeeTax = (int)($inputData['signup_fee_tax'] ?? 0);
1253 1297 $firstIterationTax = (int)($inputData['first_iteration_tax'] ?? 0);
1254 1298 $totalDiscount = (int)($inputData['total_discount'] ?? 0);
1299 + $recurringDiscount = (int)($inputData['recurring_discount'] ?? 0);
1255 1300
1256 1301 // Determine if THIS subscription item is tax-inclusive (for behavior=3 mixed carts)
1257 1302 $taxBehavior = (int) Arr::get($inputData, 'tax_behavior', 0);
1258 1303 $itemInclusive = (bool) Arr::get($inputData, 'line_meta.tax_config.inclusive', false);
@@ -1292,10 +1337,13 @@
1292 1337 }
1293 1338 } else {
1294 1339 $firstCycleCost = $recurringAmount + $signupFee - $totalDiscount;
1295 1340
1296 - if (Arr::get($inputData, 'is_recurring_coupon', 'no') === 'yes') {
1297 - $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;
1298 1346 }
1299 1347
1300 1348 if ($firstCycleCost < $recurringAmount) {
1301 1349 $adjustedTrialDays = Helper::calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval);