PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
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/Hooks/Cart/WebCheckoutHandler.php +115 -231 1.3.26 → 1.6.6 View file →
@@ -16,8 +16,9 @@
16 16 use FluentCart\App\Models\Product;
17 17 use FluentCart\App\Models\ProductVariation;
18 18 use FluentCart\App\Models\ShippingMethod;
19 19 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
20 +use FluentCart\App\Modules\Tax\TaxModule;
20 21 use FluentCart\App\Services\Localization\LocalizationManager;
21 22 use FluentCart\App\Services\Renderer\AddressSelectRenderer;
22 23 use FluentCart\App\Services\Renderer\CartDrawerRenderer;
23 24 use FluentCart\App\Services\Renderer\CartRenderer;
@@ -60,8 +61,14 @@
60 61 public function globalCheckoutRouteHandler()
61 62 {
62 63 nocache_headers();
63 64
65 + $nonce = isset($_SERVER['HTTP_X_WP_NONCE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'])) : '';
66 + if (!wp_verify_nonce($nonce, 'fluentcart') && !wp_verify_nonce($nonce, 'wp_rest')) {
67 + wp_send_json(['message' => __('Invalid nonce', 'fluent-cart')], 403);
68 + return;
69 + }
70 +
64 71 $startedAt = microtime(true);
65 72
66 73 $action = App::request()->get('fc_checkout_action');
67 74 $result = [];
@@ -190,8 +197,34 @@
190 197 if (is_wp_error($result)) {
191 198 return $result;
192 199 }
193 200
201 + $cartCouponCodes = is_array($cart->coupons) ? $cart->coupons : [];
202 + $couponWasApplied = false;
203 + foreach ($cartCouponCodes as $existingCode) {
204 + if (strcasecmp((string) $existingCode, (string) $couponCode) === 0) {
205 + $couponWasApplied = true;
206 + break;
207 + }
208 + }
209 +
210 + if (!$couponWasApplied) {
211 + $perCouponResults = is_array($result) ? Arr::get($result, 'coupon_results', []) : [];
212 + $errorMessage = '';
213 + foreach ($perCouponResults as $resultCode => $couponResult) {
214 + if (strcasecmp((string) $resultCode, (string) $couponCode) === 0) {
215 + $errorMessage = Arr::get($couponResult, 'error', '');
216 + if ($errorMessage !== '') {
217 + break;
218 + }
219 + }
220 + }
221 + if ($errorMessage === '') {
222 + $errorMessage = __('No matching coupon found for this code.', 'fluent-cart');
223 + }
224 + return new \WP_Error('coupon_not_applied', $errorMessage);
225 + }
226 +
194 227 ob_start();
195 228 (new CartSummaryRender($cart))->render($withWrapper = false);
196 229 $summary = ob_get_clean();
197 230
@@ -227,8 +260,14 @@
227 260 'content' => $modalCheckoutRender->getFragment('summary_group'),
228 261 'type' => 'replace'
229 262 ];
230 263
264 + $fragments[] = [
265 + 'selector' => '[data-fct-modal-checkout-summary]',
266 + 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
267 + 'type' => 'replace'
268 + ];
269 +
231 270 return [
232 271 'fragments' => $fragments,
233 272 'cart' => $cart,
234 273 'total' => $cartTotal,
@@ -289,9 +328,15 @@
289 328 'content' => $modalCheckoutRender->getFragment('summary_group'),
290 329 'type' => 'replace'
291 330 ];
292 331
332 + $fragments[] = [
333 + 'selector' => '[data-fct-modal-checkout-summary]',
334 + 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
335 + 'type' => 'replace'
336 + ];
293 337
338 +
294 339 return [
295 340 'fragments' => $fragments,
296 341 'total' => $cartTotal,
297 342 'formatted_total' => Helper::toDecimal($cartTotal),
@@ -364,8 +409,14 @@
364 409 $totalPrice = apply_filters('fluent_cart/cart/estimated_total', $totalPrice, [
365 410 'cart' => $cart
366 411 ]);
367 412
413 + // Prorate credit and upgrade discount (plan upgrade) are post-tax adjustments on
414 + // the payable total.
415 + $totalPrice = max(0, $totalPrice
416 + - (int) Arr::get($cart->checkout_data, 'prorate_credit.amount', 0)
417 + - (int) Arr::get($cart->checkout_data, 'upgrade_discount.amount', 0));
418 +
368 419 $fragments = [];
369 420
370 421 $cartRender = (new CheckoutRenderer($cart));
371 422 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
@@ -427,8 +478,9 @@
427 478
428 479 $oldTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
429 480 $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
430 481
482 + $previousFormData = Arr::get($cart->checkout_data, 'form_data', []);
431 483 $checkoutData = Arr::wrap($cart->checkout_data);
432 484 Arr::set($checkoutData, 'form_data.' . $type . '_address_id', $addressId);
433 485 Arr::set($checkoutData, 'form_data.' . $type . '_country', $country);
434 486 Arr::set($checkoutData, 'form_data.' . $type . '_state', $state);
@@ -438,8 +490,14 @@
438 490 Arr::set($checkoutData, 'form_data.shipping_country', $country);
439 491 Arr::set($checkoutData, 'form_data.shipping_state', $state);
440 492 }
441 493
494 + $checkoutData = (new TaxModule())->maybeInvalidateVatValidationForCountryChange($checkoutData, [
495 + 'ship_to_different' => Arr::get($previousFormData, 'ship_to_different', 'no'),
496 + 'billing_country' => Arr::get($previousFormData, 'billing_country', ''),
497 + 'shipping_country' => Arr::get($previousFormData, 'shipping_country', ''),
498 + ]);
499 +
442 500 $cart->checkout_data = $checkoutData;
443 501
444 502 $cart->save();
445 503
@@ -559,8 +617,10 @@
559 617 ob_start();
560 618
561 619 $selectedId = Arr::get($cart->checkout_data, 'shipping_data.shipping_method_id', '');
562 620
621 + $selectedId = CartHelper::resolveAutoSelectShippingMethod($cart, $availableShippingMethods ?: [], $selectedId);
622 +
563 623 if (!$availableShippingMethods || is_wp_error($availableShippingMethods)) {
564 624 (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render();
565 625 } else {
566 626 foreach ($availableShippingMethods as $method) {
@@ -581,8 +641,9 @@
581 641 'type' => 'replace'
582 642 ]
583 643 ],
584 644 'country_code' => $data['country_code'],
645 + 'shipping_method_id' => $selectedId ?: null,
585 646 ];
586 647 }
587 648
588 649 public function handleCartStatusAjax()
@@ -700,10 +761,10 @@
700 761 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
701 762 }
702 763
703 764 $allData = App::request()->all();
704 - $dataKey = (string) Arr::get($allData, 'data_key');
705 - $dataValue = (string) Arr::get($allData, 'data_value');
765 + $dataKey = sanitize_text_field((string) Arr::get($allData, 'data_key'));
766 + $dataValue = sanitize_text_field((string) Arr::get($allData, 'data_value'));
706 767
707 768 if ($dataKey) {
708 769 $allData[$dataKey] = $dataValue;
709 770 }
@@ -725,9 +786,11 @@
725 786 'order_notes' => 'form_data.order_notes',
726 787 'shipping_method_id' => 'shipping_data.shipping_method_id',
727 788 '_fct_pay_method' => 'form_data._fct_pay_method',
728 789 'billing_company_name' => 'form_data.billing_company_name',
729 - 'fct_billing_tax_id' => 'tax_data.vat_number'
790 + 'billing_legal_registration_id' => 'form_data.billing_legal_registration_id',
791 + 'is_business' => 'form_data.is_business',
792 + 'fct_billing_tax_id' => 'tax_data.vat_number',
730 793 ];
731 794
732 795 $addressFieldKeys = [
733 796 'full_name',
@@ -766,8 +829,16 @@
766 829 $normalizeData[$key] = $value;
767 830 }
768 831 }
769 832
833 + // Force-check the explicit data_key field — bulk POST may have already stored it as a side-effect.
834 + if ($dataKey && isset($validKeys[$dataKey]) && !isset($normalizeData[$dataKey])) {
835 + $storedValue = (string) Arr::get($prevCheckoutData, $validKeys[$dataKey], '');
836 + if ($storedValue !== $dataValue) {
837 + $normalizeData[$dataKey] = $dataValue;
838 + }
839 + }
840 +
770 841 if (!$normalizeData) {
771 842 return [
772 843 'message' => __('No changes detected', 'fluent-cart')
773 844 ];
@@ -783,8 +854,9 @@
783 854 foreach ($normalizeData as $normalizeKey => $normalizeValue) {
784 855 Arr::set($checkoutData, $validKeys[$normalizeKey], $normalizeValue);
785 856 }
786 857
858 + $oldTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
787 859 $fillData = [
788 860 'checkout_data' => $checkoutData,
789 861 'cart_data' => $cart->cart_data,
790 862 'hook_changes' => [
@@ -815,8 +887,10 @@
815 887 $cart->last_name = implode(' ', $nameParts);
816 888 }
817 889
818 890 $cart->fill($fillData);
891 + $newTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
892 + $taxAmountChanged = $oldTaxTotal !== $newTaxTotal;
819 893 $cart->clearFeeCache();
820 894
821 895 $cart = CartHelper::getCart();
822 896 $sanitizedUtmData = UtmHelper::getUtmDataOfRequest();
@@ -844,30 +918,36 @@
844 918 'content' => $cartRender->getFragment('cart_summary_fragment'),
845 919 'type' => 'replace'
846 920 ];
847 921
922 + if ($taxAmountChanged || !empty($hookChanges['shipping'])) {
923 + // also update the payment methods
924 + if ($enableModalCheckout === 'yes') {
925 + $fragments[] = [
926 + 'selector' => '[data-fluent-cart-checkout-payment-methods]',
927 + 'content' => $modalCheckoutRender->getFragment('payment_methods'),
928 + 'type' => 'replace'
929 + ];
930 + } else {
931 + $fragments[] = [
932 + 'selector' => '[data-fluent-cart-checkout-payment-methods]',
933 + 'content' => $cartRender->getFragment('payment_methods'),
934 + 'type' => 'replace'
935 + ];
936 + }
848 937
849 - // also update the payment methods
850 - if ($enableModalCheckout === 'yes') {
851 938 $fragments[] = [
852 - 'selector' => '[data-fluent-cart-checkout-payment-methods]',
853 - 'content' => $modalCheckoutRender->getFragment('payment_methods'),
854 - 'type' => 'replace'
939 + 'selector' => '[data-fct-modal-checkout-summary-group]',
940 + 'content' => $modalCheckoutRender->getFragment('summary_group'),
941 + 'type' => 'replace'
855 942 ];
856 - } else {
943 +
857 944 $fragments[] = [
858 - 'selector' => '[data-fluent-cart-checkout-payment-methods]',
859 - 'content' => $cartRender->getFragment('payment_methods'),
860 - 'type' => 'replace'
945 + 'selector' => '[data-fct-modal-checkout-summary]',
946 + 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
947 + 'type' => 'replace'
861 948 ];
862 949 }
863 -
864 -
865 - $fragments[] = [
866 - 'selector' => '[data-fct-modal-checkout-summary-group]',
867 - 'content' => $modalCheckoutRender->getFragment('summary_group'),
868 - 'type' => 'replace'
869 - ];
870 950 }
871 951
872 952 // Re-render cart summary on payment method change (fees may depend on payment method)
873 953 if (isset($normalizeData['_fct_pay_method'])) {
@@ -890,8 +970,14 @@
890 970 'selector' => '[data-fct-modal-checkout-summary-group]',
891 971 'content' => $modalCheckoutRender->getFragment('summary_group'),
892 972 'type' => 'replace'
893 973 ];
974 +
975 + $fragments[] = [
976 + 'selector' => '[data-fct-modal-checkout-summary]',
977 + 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
978 + 'type' => 'replace'
979 + ];
894 980 }
895 981 }
896 982
897 983 if (($dataKey === 'billing_address_id' || $dataKey === 'shipping_address_id') && $dataValue !== '') {
@@ -942,218 +1028,8 @@
942 1028 'cart' => $cart
943 1029 ];
944 1030 }
945 1031
946 - public function handleSaveCustomerDataAjax()
947 - {
948 -
949 - $key = sanitize_text_field(App::request()->get('data_key'));
950 - $value = sanitize_text_field(App::request()->get('data_value'));
951 -
952 - $cart = CartHelper::getCart();
953 - if (!$cart) {
954 - return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
955 - }
956 -
957 - $addressFieldKeys = [
958 - 'full_name',
959 - 'country',
960 - 'address_1',
961 - 'address_2',
962 - 'state',
963 - 'city',
964 - 'postcode'
965 - ];
966 -
967 - $validKeys = [
968 - 'ship_to_different' => 'form_data.ship_to_different',
969 - 'billing_email' => 'form_data.billing_email',
970 - 'billing_address_id' => 'form_data.billing_address_id',
971 - 'shipping_address_id' => 'form_data.shipping_address_id',
972 - 'billing_company' => 'form_data.billing_company',
973 - 'order_notes' => 'form_data.order_notes',
974 - 'shipping_method_id' => 'shipping_data.shipping_method_id',
975 - '_fct_pay_method' => 'form_data._fct_pay_method',
976 - ];
977 -
978 - foreach ($addressFieldKeys as $addressFieldKey) {
979 - $validKeys['billing_' . $addressFieldKey] = 'form_data.billing_' . $addressFieldKey;
980 - $validKeys['shipping_' . $addressFieldKey] = 'form_data.shipping_' . $addressFieldKey;
981 - }
982 -
983 - if (!isset($validKeys[$key])) {
984 - return new \WP_Error('invalid_key', __('Invalid data key', 'fluent-cart'));
985 - }
986 -
987 - $checkoutData = Arr::wrap($cart->checkout_data);
988 -
989 - $prevValue = Arr::get($checkoutData, $validKeys[$key], '');
990 - if ($prevValue === $value) {
991 - return [
992 - 'message' => __('Updated', 'fluent-cart')
993 - ];
994 - }
995 -
996 - $oldCheckoutData = $checkoutData;
997 - Arr::set($checkoutData, $validKeys[$key], $value);
998 -
999 - $fillData = apply_filters('fluent_cart/checkout/before_patch_checkout_data', [
1000 - 'checkout_data' => $checkoutData,
1001 - 'cart_data' => $cart->cart_data,
1002 - 'hook_changes' => [
1003 - 'shipping' => false,
1004 - 'tax' => false
1005 - ]
1006 - ], [
1007 - 'cart' => $cart,
1008 - 'key' => $key,
1009 - 'value' => $value,
1010 - 'prev_value' => $prevValue,
1011 - 'old_checkout_data' => $oldCheckoutData
1012 - ]);
1013 -
1014 - $changes = Arr::get($fillData, 'hook_changes', []);
1015 - unset($fillData['hook_changes']);
1016 -
1017 -
1018 - $cart->fill($fillData);
1019 - $cart->save();
1020 - $fragments = [];
1021 -
1022 -
1023 - if (Arr::get($changes, 'shipping', false)) {
1024 - $fragments[] = [
1025 - 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]',
1026 - 'content' => (new CheckoutRenderer($cart))->getFragment('shipping_methods'),
1027 - 'type' => 'replace'
1028 - ];
1029 - }
1030 - //(new CartSummaryRender($cart))->render(false);
1031 -
1032 - ob_start();
1033 - (new CartSummaryRender($cart))->render(false);
1034 - $render = ob_get_clean();
1035 - if (array_filter($changes)) {
1036 - $fragments[] = [
1037 - 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
1038 - //'content' => (new CheckoutRender($cart))->getFragment('cart_summary_fragment'),
1039 - 'content' => $render,
1040 - 'type' => 'replace'
1041 - ];
1042 - }
1043 -
1044 - return [
1045 - 'fragments' => $fragments,
1046 - 'message' => __('Data saved successfully', 'fluent-cart'),
1047 - 'tax_total_Changes' => Arr::get($changes, 'tax', false),
1048 - 'shipping_charge_changes' => Arr::get($changes, 'shipping', false),
1049 - 'notify' => false,
1050 - 'cart' => $cart
1051 - ];
1052 -
1053 - // Shipping Module
1054 -
1055 - add_action('fluent_cart/checkout/customer_data_saved', function ($data) {
1056 - // Recalculate tax if needed
1057 -
1058 - do_action('fluent_cart/checkout/tax_data_changed', $data);
1059 -
1060 - });
1061 -
1062 - do_action('fluent_cart/checkout/customer_data_saved', [
1063 - 'cart' => $cart,
1064 - 'key' => $key,
1065 - 'value' => $value,
1066 - 'old_value' => $prevValue,
1067 - 'old_data' => $oldCheckoutData
1068 - ]);
1069 -
1070 - $didTaxChanges = did_action('fluent_cart/checkout/tax_data_changed');
1071 - $didShippingChanges = did_action('fluent_cart/checkout/shipping_data_changed');
1072 -
1073 -
1074 - if ($cart) {
1075 - $checkoutRender = new CheckoutRenderer($cart);
1076 - $shippingChanged = false;
1077 - // we have to decide for which keys we will update shipping option
1078 - $fragments = [];
1079 - if ($cart->requireShipping()) {
1080 - $watchingKeys = ['ship_to_different', 'billing_address', 'billing_country', 'billing_state'];
1081 - if (Arr::get($cart->checkout_data, 'form_data.ship_to_different', null) === 'yes') {
1082 - $watchingKeys = ['ship_to_different', 'shipping_address', 'shipping_country', 'shipping_state'];
1083 - }
1084 -
1085 - if (in_array($key, $watchingKeys)) {
1086 - $fragments[] = [
1087 - 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]',
1088 - 'content' => $checkoutRender->getFragment('shipping_methods'),
1089 - 'type' => 'replace'
1090 - ];
1091 - }
1092 -
1093 - $shippingChanged = true;
1094 - }
1095 -
1096 - $oldTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
1097 - $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
1098 -
1099 - $countryCode = Arr::get($cart->checkout_data, 'form_data.shipping_country');
1100 -
1101 - if (!$countryCode) {
1102 - $countryCode = Arr::get($cart->checkout_data, 'form_data.billing_country');
1103 - }
1104 -
1105 - if (Arr::get($cart->checkout_data, 'form_data.ship_to_different', 'no') === 'no') {
1106 - $countryCode = Arr::get($cart->checkout_data, 'form_data.billing_country');
1107 - }
1108 -
1109 - $data = [
1110 - 'country_code' => $countryCode,
1111 - 'timezone' => ''
1112 - ];
1113 -
1114 - $list = $this->getShippingMethodsListView($data);
1115 -
1116 - if ($list === false) {
1117 - $cart->checkout_data = array_merge($cart->checkout_data, [
1118 - 'shipping_data' => [
1119 - 'shipping_method_id' => null,
1120 - 'shipping_charge' => 0
1121 - ]
1122 - ]);
1123 - $cart->save();
1124 - }
1125 -
1126 - do_action('fluent_cart/checkout/form_data_changed', [
1127 - 'cart' => $cart
1128 - ]);
1129 -
1130 - $fragments[] = [
1131 - 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
1132 - 'content' => $checkoutRender->getFragment('cart_summary_fragment'),
1133 - 'type' => 'replace'
1134 - ];
1135 -
1136 - $newTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
1137 - $newShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
1138 -
1139 - $checkoutData = [
1140 - 'fragments' => $fragments,
1141 - 'message' => __('Data saved successfully', 'fluent-cart'),
1142 - 'tax_total_Changes' => $oldTaxTotal != $newTaxTotal,
1143 - 'shipping_charge_changes' => $oldShippingCharge != $newShippingCharge,
1144 - 'notify' => false
1145 - ];
1146 -
1147 - return apply_filters('fluent_cart/checkout/checkout_data_changed', $checkoutData, ['cart' => $cart]);
1148 - }
1149 -
1150 - return [
1151 - 'message' => __('Failed to save data', 'fluent-cart'),
1152 - 'notify' => false
1153 - ];
1154 - }
1155 -
1156 1032 public function handleOrderBumpRequest()
1157 1033 {
1158 1034 $requestData = App::request()->all();
1159 1035
@@ -1162,9 +1038,9 @@
1162 1038 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
1163 1039 }
1164 1040
1165 1041 $checkoutData = $cart->checkout_data;
1166 - if (!empty($checkoutData['upgrade_data']) || !empty($checkoutData['is_locked'])) {
1042 + if (!$cart->acceptsAdditionalItems()) {
1167 1043 return new \WP_Error('invalid_request', __('This cart is locked or already has an upgrade applied.', 'fluent-cart'));
1168 1044 }
1169 1045
1170 1046 $upgradeFromVariationId = (int) Arr::get($requestData, 'upgrade_form', 0);
@@ -1179,8 +1055,14 @@
1179 1055 'request_data' => $requestData
1180 1056 ]);
1181 1057 }
1182 1058
1059 + // A locked cart may still take bumps through the accepts_additional_items
1060 + // filter, but must never let an item it pinned be swapped out.
1061 + if (Arr::get($checkoutData, 'is_locked') === 'yes') {
1062 + return new \WP_Error('invalid_request', __('This cart is locked and cannot be modified.', 'fluent-cart'));
1063 + }
1064 +
1183 1065 if (!$upgradeFromVariationId || !$targetVariationId) {
1184 1066 return new \WP_Error('invalid_request', __('Invalid upgrade request.', 'fluent-cart'));
1185 1067 }
1186 1068
@@ -1259,10 +1141,12 @@
1259 1141 $sanitizedData[$dataKey] = '';
1260 1142 } else {
1261 1143 $sanitizedData[$dataKey] = sanitize_email($dataValue ?? '');
1262 1144 }
1263 - } else if (in_array($dataKey, ['billing_company_name', 'shipping_company_name'])) {
1145 + } else if (in_array($dataKey, ['billing_company_name', 'shipping_company_name', 'billing_legal_registration_id'])) {
1264 1146 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1147 + } else if ($dataKey === 'is_business') {
1148 + $sanitizedData[$dataKey] = $dataValue === 'yes' ? 'yes' : 'no';
1265 1149 } else if ($dataKey === 'billing_state' && !empty($dataValue)) {
1266 1150 $billingCountry = Arr::get($allData, 'billing_country');
1267 1151
1268 1152 $states = LocalizationManager::getInstance()->statesOptions($billingCountry);