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 +62 -63 1.6.0 → 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'],
@@ -431,15 +448,12 @@
431 448
432 449 static::syncCustomerNames($order, $args);
433 450 $cart = CartHelper::getCart();
434 451
435 - $utmData = [];
436 - if (!empty($cart) && is_array($cart->utm_data) && count($cart->utm_data) > 0) {
437 - $utmData = $cart->utm_data;
438 - }
439 -
440 - $requestUtmData = UtmHelper::getUtmDataOfRequest();
441 - $utmData = wp_parse_args($requestUtmData, $utmData);
452 + $utmData = UtmHelper::resolveUtmData(
453 + UtmHelper::getUtmDataOfRequest(),
454 + !empty($cart) ? $cart->utm_data : []
455 + );
442 456 UtmHelper::addUtmToOrder($order->id, $utmData);
443 457
444 458 $prevOrder = Arr::get($args, 'prev_order', null);
445 459
@@ -526,9 +540,10 @@
526 540 private static function syncCustomerNames($order, $args)
527 541 {
528 542 $customer = $order->customer;
529 543
530 - 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())) {
531 546 return;
532 547 }
533 548
534 549 $firstName = Arr::get($args, 'billing_address.first_name');
@@ -538,18 +553,13 @@
538 553 'first_name' => $firstName,
539 554 'last_name' => $lastName,
540 555 ]);
541 556
542 - $user = get_user_by('email', $customer->email);
543 -
544 - if (empty($user)) {
545 - 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);
546 561 }
547 -
548 - if (is_user_logged_in() && $user->ID === get_current_user_id()) {
549 - update_user_meta($user->ID, 'first_name', $firstName);
550 - update_user_meta($user->ID, 'last_name', $lastName);
551 - }
552 562 }
553 563
554 564 public static function updateStock($order)
555 565 {
@@ -577,16 +587,18 @@
577 587 if ($current_user->ID) {
578 588 $billingAddress['email'] = $current_user->user_email;
579 589 $billingAddress['user_id'] = $current_user->ID;
580 590 } else {
581 - static::handleUserCreation($orderData, $billingAddress);
591 + unset($billingAddress['user_id']);
582 592 }
583 593
584 594 $customer = CustomerResource::create($billingAddress);
585 595 $customer = Arr::get($customer, 'data', null);
586 596 $customerId = Arr::get($customer, 'id', null);
587 - static::createCustomerAddress($billingAddress, $customerId);
588 - static::createCustomerAddress($shippingAddress, $customerId);
597 + if ($customer && $customer->wasRecentlyCreated) {
598 + static::createCustomerAddress($billingAddress, $customerId);
599 + static::createCustomerAddress($shippingAddress, $customerId);
600 + }
589 601
590 602 return $customer;
591 603 }
592 604
@@ -591,17 +603,13 @@
591 603 }
592 604
593 605 private static function updateExistingCustomer($customer, $orderData, $billingAddress, $shippingAddress)
594 606 {
595 - if (empty($customer->user_id)) {
596 - $currentLoggedInUser = wp_get_current_user();
597 - if ($currentLoggedInUser && $currentLoggedInUser->user_email === $customer->email) {
598 - $userId = get_current_user_id();
599 - $customer->update(['user_id' => $userId]);
600 - $billingAddress['user_id'] = $userId;
601 - }
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;
602 611 }
603 -
604 612 $customer->load(['billing_address', 'shipping_address']);
605 613
606 614 if ($customer->billing_address->count() < 1) {
607 615 static::createCustomerAddress($billingAddress, $customer->id);
@@ -608,25 +616,10 @@
608 616 }
609 617 if ($customer->shipping_address->count() < 1) {
610 618 static::createCustomerAddress($shippingAddress, $customer->id);
611 619 }
612 -
613 - static::handleUserCreation($orderData, $billingAddress, $customer);
614 620 }
615 621
616 - private static function handleUserCreation($orderData, &$billingAddress, $customer = null)
617 - {
618 - $userEmail = Arr::get($billingAddress, 'email');
619 - $user = get_user_by('email', $userEmail);
620 -
621 - if ($user) {
622 - $billingAddress['user_id'] = $user->ID;
623 - if ($customer) {
624 - $customer->update(['user_id' => $user->ID]);
625 - }
626 - }
627 - }
628 -
629 622 private static function getCustomerEmail($billingAddress)
630 623 {
631 624 return is_user_logged_in() ? wp_get_current_user()->user_email : $billingAddress['email'];
632 625 }
@@ -986,10 +979,9 @@
986 979 if (empty($data['billing_email']) || !is_email($data['billing_email'])) {
987 980 $errors['billing_email']['invalid'] = __('Email must be a valid email address.', 'fluent-cart');
988 981 }
989 982
990 - if (CheckoutFieldsSchema::isFullNameRequired() || !empty($data['billing_full_name'])) {
991 - // Modal checkout always sends billing_full_name regardless of store name field settings
983 + if (CheckoutFieldsSchema::isFullNameRequired()) {
992 984 if (empty($data['billing_full_name'])) {
993 985 $errors['billing_full_name']['required'] = __('Full name is required.', 'fluent-cart');
994 986 }
995 987 } else {
@@ -1006,9 +998,16 @@
1006 998
1007 999
1008 1000 if ($cart->requireShipping()) {
1009 1001 if (!empty($data['fc_selected_shipping_method'])) {
1010 - $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 +
1011 1010 $shippingCountry = Arr::get($data, 'billing_country', '');
1012 1011 $shippingState = Arr::get($data, 'billing_state', '');
1013 1012 $shipToDifferent = Arr::get($data, 'ship_to_different', 'no') === 'yes';
1014 1013
@@ -1024,9 +1023,9 @@
1024 1023 $errors['shipping_method']['unavailable'] = __('We don\'t ship to this address. Please select a different address.', 'fluent-cart');
1025 1024 } else {
1026 1025 $found = false;
1027 1026 foreach ($availableShippingMethods as $shippingMethod) {
1028 - if ($shippingMethod->id == $selectedMethod) {
1027 + if ((int) $shippingMethod->id === $selectedMethod) {
1029 1028 $found = true;
1030 1029 break;
1031 1030 }
1032 1031 }