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 | api/Checkout/CheckoutApi.php +58 -56 1.6.3 → 1.7.1 View file →
@@ -21,8 +21,9 @@
21 21 use FluentCart\App\Models\Order;
22 22 use FluentCart\App\Models\OrderAddress;
23 23 use FluentCart\App\Models\ShippingMethod;
24 24 use FluentCart\App\Services\CheckoutService;
25 +use FluentCart\App\Services\CustomerIdentity\EmailVerificationService;
25 26 use FluentCart\App\Services\Localization\LocalizationManager;
26 27 use FluentCart\App\Services\OrderService;
27 28 use FluentCart\App\Services\Payments\PaymentHelper;
28 29 use FluentCart\App\Services\Payments\PaymentInstance;
@@ -72,8 +73,19 @@
72 73
73 74 $cart = $cart->reValidateCoupons();
74 75
75 76 $cartData = $cart->cart_data;
77 +
78 + // Carts stored before the quantity ceiling existed can still hold an overflowing line.
79 + foreach ($cartData as $cartItem) {
80 + $quantityError = CartHelper::validateQuantity(Arr::get($cartItem, 'quantity', 1));
81 + if ($quantityError) {
82 + wp_send_json([
83 + 'status' => 'failed',
84 + 'message' => $quantityError->get_error_message(),
85 + ], 422);
86 + }
87 + }
76 88 $prevOrder = $cart->order;
77 89 if ($prevOrder) {
78 90 $prevOrder->load('order_items');
79 91 }
@@ -119,8 +131,12 @@
119 131 'message' => $validation->get_error_message(),
120 132 ], 403);
121 133 }
122 134
135 + // order_id unlocks another order's addresses in prepareAddressData(), so it
136 + // may only come from this cart's own order, never from the request.
137 + unset($data['order_id']);
138 +
123 139 if (empty($data['billing_address_id'])) {
124 140 if ($prevOrder instanceof Order) {
125 141 $oldCustomer = $prevOrder->customer;
126 142 if ($oldCustomer) {
@@ -149,23 +165,22 @@
149 165 ]);
150 166 }
151 167
152 168 if (!CheckoutFieldsSchema::isFullNameRequired()) {
153 - if (!empty($validatedData['billing_full_name']) && empty($validatedData['billing_first_name'])) {
154 - // Modal checkout sends billing_full_name — split into first/last name
155 - $nameParts = explode(' ', $validatedData['billing_full_name'], 2);
156 - $validatedData['billing_first_name'] = $nameParts[0];
157 - $validatedData['billing_last_name'] = $nameParts[1] ?? '';
158 - } else {
159 - $validatedData['billing_full_name'] = trim(
160 - Arr::get($validatedData, 'billing_first_name') . ' ' . Arr::get($validatedData, 'billing_last_name')
161 - );
162 - }
169 + // First/Last name mode: the form posts those fields; the full name is derived from them.
170 + $validatedData['billing_full_name'] = trim(
171 + Arr::get($validatedData, 'billing_first_name') . ' ' . Arr::get($validatedData, 'billing_last_name')
172 + );
163 173 }
164 174
165 175 $orderData = OrderService::groupSanitizedData($validatedData);
166 176
167 - $shippingMethodId = Arr::get($orderData, 'others.fc_shipping_method');
177 + // The form posts the method twice: the checked radio (fc_shipping_method) and its
178 + // hidden mirror (fc_selected_shipping_method). validateData() checks only the mirror
179 + // against the address's zones, so pricing from the radio let a request pass with one
180 + // method and be charged by another, from a zone the address is not in. Read from
181 + // $validatedData, not the sanitized copy in others: that is the exact integer checked.
182 + $shippingMethodId = (int) Arr::get($validatedData, 'fc_selected_shipping_method', 0);
168 183
169 184 $shippingMethod = null;
170 185 $shippingCharge = 0;
171 186 if (!$cartCheckoutService->isAllDigital()) {
@@ -291,14 +306,16 @@
291 306 }
292 307
293 308 private static function getOrCreateCustomer(CartCheckoutHelper $cartCheckoutHelper, $orderData)
294 309 {
295 - $customerEmail = static::getCustomerEmail($orderData['billing_address']);
296 - if (is_user_logged_in()) {
297 - $customerEmail = wp_get_current_user()->user_email;
298 - Arr::set($orderData, 'billing_address.email', $customerEmail);
310 + $customer = is_user_logged_in() ? ApiCustomerResource::getCurrentCustomer() : null;
311 + $email = static::getCustomerEmail($orderData['billing_address']);
312 + Arr::set($orderData, 'billing_address.email', $email);
313 + if (!$customer) {
314 + // Reuse the email's customer for the purchase without granting ownership.
315 + $customer = Customer::query()->where('email', $email)->orderBy('id')->first();
299 316 }
300 - $customer = $cartCheckoutHelper->getCustomer($customerEmail);
317 +
301 318 return static::createCustomerWithAddress(
302 319 $customer,
303 320 $orderData,
304 321 $orderData['billing_address'],
@@ -523,9 +540,10 @@
523 540 private static function syncCustomerNames($order, $args)
524 541 {
525 542 $customer = $order->customer;
526 543
527 - if (empty($customer)) {
544 + if (empty($customer) || !is_user_logged_in() || (int) $customer->user_id !== get_current_user_id()
545 + || EmailVerificationService::isRequired(get_current_user_id())) {
528 546 return;
529 547 }
530 548
531 549 $firstName = Arr::get($args, 'billing_address.first_name');
@@ -535,18 +553,13 @@
535 553 'first_name' => $firstName,
536 554 'last_name' => $lastName,
537 555 ]);
538 556
539 - $user = get_user_by('email', $customer->email);
540 -
541 - if (empty($user)) {
542 - return;
557 + // Keep profile updates tied to the buyer's stored account link too.
558 + if (is_user_logged_in() && (int) $customer->user_id === get_current_user_id()) {
559 + update_user_meta(get_current_user_id(), 'first_name', $firstName);
560 + update_user_meta(get_current_user_id(), 'last_name', $lastName);
543 561 }
544 -
545 - if (is_user_logged_in() && $user->ID === get_current_user_id()) {
546 - update_user_meta($user->ID, 'first_name', $firstName);
547 - update_user_meta($user->ID, 'last_name', $lastName);
548 - }
549 562 }
550 563
551 564 public static function updateStock($order)
552 565 {
@@ -574,16 +587,18 @@
574 587 if ($current_user->ID) {
575 588 $billingAddress['email'] = $current_user->user_email;
576 589 $billingAddress['user_id'] = $current_user->ID;
577 590 } else {
578 - static::handleUserCreation($orderData, $billingAddress);
591 + unset($billingAddress['user_id']);
579 592 }
580 593
581 594 $customer = CustomerResource::create($billingAddress);
582 595 $customer = Arr::get($customer, 'data', null);
583 596 $customerId = Arr::get($customer, 'id', null);
584 - static::createCustomerAddress($billingAddress, $customerId);
585 - static::createCustomerAddress($shippingAddress, $customerId);
597 + if ($customer && $customer->wasRecentlyCreated) {
598 + static::createCustomerAddress($billingAddress, $customerId);
599 + static::createCustomerAddress($shippingAddress, $customerId);
600 + }
586 601
587 602 return $customer;
588 603 }
589 604
@@ -588,17 +603,13 @@
588 603 }
589 604
590 605 private static function updateExistingCustomer($customer, $orderData, $billingAddress, $shippingAddress)
591 606 {
592 - if (empty($customer->user_id)) {
593 - $currentLoggedInUser = wp_get_current_user();
594 - if ($currentLoggedInUser && $currentLoggedInUser->user_email === $customer->email) {
595 - $userId = get_current_user_id();
596 - $customer->update(['user_id' => $userId]);
597 - $billingAddress['user_id'] = $userId;
598 - }
607 + // Order addresses come from this checkout; saved profile data needs proof.
608 + if (!is_user_logged_in() || (int) $customer->user_id !== get_current_user_id()
609 + || EmailVerificationService::isRequired(get_current_user_id())) {
610 + return;
599 611 }
600 -
601 612 $customer->load(['billing_address', 'shipping_address']);
602 613
603 614 if ($customer->billing_address->count() < 1) {
604 615 static::createCustomerAddress($billingAddress, $customer->id);
@@ -605,25 +616,10 @@
605 616 }
606 617 if ($customer->shipping_address->count() < 1) {
607 618 static::createCustomerAddress($shippingAddress, $customer->id);
608 619 }
609 -
610 - static::handleUserCreation($orderData, $billingAddress, $customer);
611 620 }
612 621
613 - private static function handleUserCreation($orderData, &$billingAddress, $customer = null)
614 - {
615 - $userEmail = Arr::get($billingAddress, 'email');
616 - $user = get_user_by('email', $userEmail);
617 -
618 - if ($user) {
619 - $billingAddress['user_id'] = $user->ID;
620 - if ($customer) {
621 - $customer->update(['user_id' => $user->ID]);
622 - }
623 - }
624 - }
625 -
626 622 private static function getCustomerEmail($billingAddress)
627 623 {
628 624 return is_user_logged_in() ? wp_get_current_user()->user_email : $billingAddress['email'];
629 625 }
@@ -983,10 +979,9 @@
983 979 if (empty($data['billing_email']) || !is_email($data['billing_email'])) {
984 980 $errors['billing_email']['invalid'] = __('Email must be a valid email address.', 'fluent-cart');
985 981 }
986 982
987 - if (CheckoutFieldsSchema::isFullNameRequired() || !empty($data['billing_full_name'])) {
988 - // Modal checkout always sends billing_full_name regardless of store name field settings
983 + if (CheckoutFieldsSchema::isFullNameRequired()) {
989 984 if (empty($data['billing_full_name'])) {
990 985 $errors['billing_full_name']['required'] = __('Full name is required.', 'fluent-cart');
991 986 }
992 987 } else {
@@ -1003,9 +998,16 @@
1003 998
1004 999
1005 1000 if ($cart->requireShipping()) {
1006 1001 if (!empty($data['fc_selected_shipping_method'])) {
1007 - $selectedMethod = $data['fc_selected_shipping_method'];
1002 + // One integer, decided here, is both what is checked and what placeOrder() prices.
1003 + // A loose compare let PHP 7.4 match "1<b>2" to method 1, and sanitize_text_field()
1004 + // then turned the same string into "12", so the order was priced by method 12.
1005 + $rawMethod = $data['fc_selected_shipping_method'];
1006 + $isPlainId = (is_string($rawMethod) || is_int($rawMethod)) && (string) absint($rawMethod) === (string) $rawMethod;
1007 + $selectedMethod = $isPlainId ? absint($rawMethod) : 0;
1008 + $data['fc_selected_shipping_method'] = $selectedMethod;
1009 +
1008 1010 $shippingCountry = Arr::get($data, 'billing_country', '');
1009 1011 $shippingState = Arr::get($data, 'billing_state', '');
1010 1012 $shipToDifferent = Arr::get($data, 'ship_to_different', 'no') === 'yes';
1011 1013
@@ -1021,9 +1023,9 @@
1021 1023 $errors['shipping_method']['unavailable'] = __('We don\'t ship to this address. Please select a different address.', 'fluent-cart');
1022 1024 } else {
1023 1025 $found = false;
1024 1026 foreach ($availableShippingMethods as $shippingMethod) {
1025 - if ($shippingMethod->id == $selectedMethod) {
1027 + if ((int) $shippingMethod->id === $selectedMethod) {
1026 1028 $found = true;
1027 1029 break;
1028 1030 }
1029 1031 }