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 +68 -63 1.5.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'],
@@ -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
@@ -459,8 +473,14 @@
459 473 }
460 474
461 475 $paymentInstance = new PaymentInstance($order);
462 476
477 + // Transition subscription from pending → intended before submitting to the gateway
478 + if ($paymentInstance->subscription && $paymentInstance->subscription->status === Status::SUBSCRIPTION_PENDING) {
479 + $paymentInstance->subscription->status = Status::SUBSCRIPTION_INTENDED;
480 + $paymentInstance->subscription->save();
481 + }
482 +
463 483 $data = $gateway->makePaymentFromPaymentInstance($paymentInstance);
464 484
465 485 if (is_wp_error($data)) {
466 486 // Server-observed create failure: mark the transaction FAILED so the next
@@ -520,9 +540,10 @@
520 540 private static function syncCustomerNames($order, $args)
521 541 {
522 542 $customer = $order->customer;
523 543
524 - 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())) {
525 546 return;
526 547 }
527 548
528 549 $firstName = Arr::get($args, 'billing_address.first_name');
@@ -532,18 +553,13 @@
532 553 'first_name' => $firstName,
533 554 'last_name' => $lastName,
534 555 ]);
535 556
536 - $user = get_user_by('email', $customer->email);
537 -
538 - if (empty($user)) {
539 - 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);
540 561 }
541 -
542 - if (is_user_logged_in() && $user->ID === get_current_user_id()) {
543 - update_user_meta($user->ID, 'first_name', $firstName);
544 - update_user_meta($user->ID, 'last_name', $lastName);
545 - }
546 562 }
547 563
548 564 public static function updateStock($order)
549 565 {
@@ -571,16 +587,18 @@
571 587 if ($current_user->ID) {
572 588 $billingAddress['email'] = $current_user->user_email;
573 589 $billingAddress['user_id'] = $current_user->ID;
574 590 } else {
575 - static::handleUserCreation($orderData, $billingAddress);
591 + unset($billingAddress['user_id']);
576 592 }
577 593
578 594 $customer = CustomerResource::create($billingAddress);
579 595 $customer = Arr::get($customer, 'data', null);
580 596 $customerId = Arr::get($customer, 'id', null);
581 - static::createCustomerAddress($billingAddress, $customerId);
582 - static::createCustomerAddress($shippingAddress, $customerId);
597 + if ($customer && $customer->wasRecentlyCreated) {
598 + static::createCustomerAddress($billingAddress, $customerId);
599 + static::createCustomerAddress($shippingAddress, $customerId);
600 + }
583 601
584 602 return $customer;
585 603 }
586 604
@@ -585,17 +603,13 @@
585 603 }
586 604
587 605 private static function updateExistingCustomer($customer, $orderData, $billingAddress, $shippingAddress)
588 606 {
589 - if (empty($customer->user_id)) {
590 - $currentLoggedInUser = wp_get_current_user();
591 - if ($currentLoggedInUser && $currentLoggedInUser->user_email === $customer->email) {
592 - $userId = get_current_user_id();
593 - $customer->update(['user_id' => $userId]);
594 - $billingAddress['user_id'] = $userId;
595 - }
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;
596 611 }
597 -
598 612 $customer->load(['billing_address', 'shipping_address']);
599 613
600 614 if ($customer->billing_address->count() < 1) {
601 615 static::createCustomerAddress($billingAddress, $customer->id);
@@ -602,25 +616,10 @@
602 616 }
603 617 if ($customer->shipping_address->count() < 1) {
604 618 static::createCustomerAddress($shippingAddress, $customer->id);
605 619 }
606 -
607 - static::handleUserCreation($orderData, $billingAddress, $customer);
608 620 }
609 621
610 - private static function handleUserCreation($orderData, &$billingAddress, $customer = null)
611 - {
612 - $userEmail = Arr::get($billingAddress, 'email');
613 - $user = get_user_by('email', $userEmail);
614 -
615 - if ($user) {
616 - $billingAddress['user_id'] = $user->ID;
617 - if ($customer) {
618 - $customer->update(['user_id' => $user->ID]);
619 - }
620 - }
621 - }
622 -
623 622 private static function getCustomerEmail($billingAddress)
624 623 {
625 624 return is_user_logged_in() ? wp_get_current_user()->user_email : $billingAddress['email'];
626 625 }
@@ -980,10 +979,9 @@
980 979 if (empty($data['billing_email']) || !is_email($data['billing_email'])) {
981 980 $errors['billing_email']['invalid'] = __('Email must be a valid email address.', 'fluent-cart');
982 981 }
983 982
984 - if (CheckoutFieldsSchema::isFullNameRequired() || !empty($data['billing_full_name'])) {
985 - // Modal checkout always sends billing_full_name regardless of store name field settings
983 + if (CheckoutFieldsSchema::isFullNameRequired()) {
986 984 if (empty($data['billing_full_name'])) {
987 985 $errors['billing_full_name']['required'] = __('Full name is required.', 'fluent-cart');
988 986 }
989 987 } else {
@@ -1000,9 +998,16 @@
1000 998
1001 999
1002 1000 if ($cart->requireShipping()) {
1003 1001 if (!empty($data['fc_selected_shipping_method'])) {
1004 - $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 +
1005 1010 $shippingCountry = Arr::get($data, 'billing_country', '');
1006 1011 $shippingState = Arr::get($data, 'billing_state', '');
1007 1012 $shipToDifferent = Arr::get($data, 'ship_to_different', 'no') === 'yes';
1008 1013
@@ -1018,9 +1023,9 @@
1018 1023 $errors['shipping_method']['unavailable'] = __('We don\'t ship to this address. Please select a different address.', 'fluent-cart');
1019 1024 } else {
1020 1025 $found = false;
1021 1026 foreach ($availableShippingMethods as $shippingMethod) {
1022 - if ($shippingMethod->id == $selectedMethod) {
1027 + if ((int) $shippingMethod->id === $selectedMethod) {
1023 1028 $found = true;
1024 1029 break;
1025 1030 }
1026 1031 }