| @@ -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 | |
| @@ -622,9 +652,16 @@ | ||
| 622 | 652 | |
| 623 | 653 | $discountTotal = (int)Arr::get($cartItem, 'manual_discount', 0) + (int)Arr::get($cartItem, 'coupon_discount', 0); |
| 624 | 654 | $shippingCharge = (int)Arr::get($cartItem, 'shipping_charge', 0); |
| 625 | 655 | |
| 626 | - $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; | |
| 627 | 664 | $args = Arr::get($cartItem, 'other_info', []); |
| 628 | 665 | $paymentType = Arr::get($args, 'payment_type', 'default'); |
| 629 | 666 | |
| 630 | 667 | $postTitle = Arr::get($cartItem, 'product_title', ''); |
| @@ -935,8 +972,24 @@ | ||
| 935 | 972 | 'recurring_discount' => $recurringDiscountAmount, |
| 936 | 973 | 'total_discount' => $discountTotal |
| 937 | 974 | ]); |
| 938 | 975 | |
| 976 | + // A shipping pricing rule discounts this order's postage only; renewals bill the full charge. | |
| 977 | + $shippingDiscount = $isPhysicalProduct ? (int)Arr::get($this->args, 'shipping_discount', 0) : 0; | |
| 978 | + if ($shippingDiscount > 0) { | |
| 979 | + $renewalTotal = $recurringTotal + $shippingDiscount; | |
| 980 | + // A fully discounted charge left its shipping tax out above as well. | |
| 981 | + if ($shippingCharge <= 0) { | |
| 982 | + $storeTaxBehavior = (int)Arr::get($this->args, 'store_tax_behavior', $taxBehavior); | |
| 983 | + if ($taxBehavior === 1 || ($taxBehavior === 3 && $storeTaxBehavior === 1)) { | |
| 984 | + $renewalTotal += (int)Arr::get($this->args, 'shipping_tax', 0); | |
| 985 | + } | |
| 986 | + } | |
| 987 | + | |
| 988 | + $subscriptionPricing = $this->billRenewalsFullShipping($subscriptionPricing, $recurringTotal, $renewalTotal, $shippingDiscount); | |
| 989 | + $recurringTotal = $renewalTotal; | |
| 990 | + } | |
| 991 | + | |
| 939 | 992 | // removable upon discussion |
| 940 | 993 | $subscriptionItem = [ |
| 941 | 994 | 'product_id' => $item['post_id'], |
| 942 | 995 | 'current_payment_method' => Arr::get($this->orderData, 'payment_method'), |
| @@ -982,8 +1035,44 @@ | ||
| 982 | 1035 | |
| 983 | 1036 | $this->subscriptionData = $subscriptionData; |
| 984 | 1037 | } |
| 985 | 1038 | |
| 1039 | + /** | |
| 1040 | + * Raise the per-cycle price to the full postage while keeping the first payment. | |
| 1041 | + * | |
| 1042 | + * A gateway takes the signup fee alone during a trial, otherwise one cycle plus the | |
| 1043 | + * signup fee. So without a trial the fee absorbs the difference, or the first cycle | |
| 1044 | + * becomes a simulated trial, the same way a first-cycle coupon is expressed. | |
| 1045 | + * | |
| 1046 | + * @param array $pricing From convertToSubscriptionFormat(), built on the discounted postage. | |
| 1047 | + * @param int $firstCycleTotal Recurring total on the discounted postage, in cents. | |
| 1048 | + * @param int $renewalTotal Recurring total on the full postage, in cents. | |
| 1049 | + * @param int $shippingDiscount In cents. | |
| 1050 | + * @return array | |
| 1051 | + */ | |
| 1052 | + private function billRenewalsFullShipping(array $pricing, $firstCycleTotal, $renewalTotal, $shippingDiscount) | |
| 1053 | + { | |
| 1054 | + $pricing['recurring_amount'] = (int)$pricing['recurring_amount'] + $shippingDiscount; | |
| 1055 | + | |
| 1056 | + if ((int)$pricing['trial_days'] > 0) { | |
| 1057 | + return $pricing; | |
| 1058 | + } | |
| 1059 | + | |
| 1060 | + $extra = $renewalTotal - $firstCycleTotal; | |
| 1061 | + $signupFee = (int)$pricing['signup_fee']; | |
| 1062 | + | |
| 1063 | + if ($signupFee >= $extra) { | |
| 1064 | + $pricing['signup_fee'] = $signupFee - $extra; | |
| 1065 | + return $pricing; | |
| 1066 | + } | |
| 1067 | + | |
| 1068 | + $pricing['trial_days'] = Helper::calculateAdjustedTrialDaysForInterval(0, $pricing['billing_interval']); | |
| 1069 | + $pricing['is_trial_days_simulated'] = 'yes'; | |
| 1070 | + $pricing['signup_fee'] = $firstCycleTotal + $signupFee; | |
| 1071 | + | |
| 1072 | + return $pricing; | |
| 1073 | + } | |
| 1074 | + | |
| 986 | 1075 | private function determineCollectionMethod(): string |
| 987 | 1076 | { |
| 988 | 1077 | if (SubscriptionManagementMode::isStoreManaged()) { |
| 989 | 1078 | $paymentMethod = Arr::get($this->orderData, 'payment_method', ''); |
| @@ -1070,8 +1159,9 @@ | ||
| 1070 | 1159 | 'user_tz' => Arr::get($this->args, 'user_tz', ''), |
| 1071 | 1160 | 'create_account_after_paid' => Arr::get($this->args, 'create_account_after_paid', 'no'), |
| 1072 | 1161 | 'shipping_method_id' => Arr::get($this->args, 'shipping_method_id', 0), |
| 1073 | 1162 | 'shipping_method_title' => Arr::get($this->args, 'shipping_method_title', ''), |
| 1163 | + 'shipping_discount' => (int) Arr::get($this->args, 'shipping_discount', 0), | |
| 1074 | 1164 | 'prorate_credit' => $this->prorateCreditTotal, |
| 1075 | 1165 | 'upgrade_discount' => $this->upgradeDiscountTotal, |
| 1076 | 1166 | ], |
| 1077 | 1167 | ]; |
| @@ -1125,8 +1215,14 @@ | ||
| 1125 | 1215 | $orderData = apply_filters('fluent_cart/checkout/order_data', $orderData, [ |
| 1126 | 1216 | 'items' => $this->formattedIOrderItems, |
| 1127 | 1217 | 'args' => $this->args, |
| 1128 | 1218 | ]); |
| 1219 | + | |
| 1220 | + if (!$this->isSafeAmount(Arr::get($orderData, 'total_amount', 0))) { | |
| 1221 | + $this->validationError = $this->unsafeAmountError(); | |
| 1222 | + | |
| 1223 | + return; | |
| 1224 | + } | |
| 1129 | 1225 | |
| 1130 | 1226 | $this->orderData = $orderData; |
| 1131 | 1227 | } |
| 1132 | 1228 | |