PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.25
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.25
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 trunk All 48 releases
fluent-cart / api / Checkout / CheckoutApi.php

CheckoutApi.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.25, at api/Checkout/CheckoutApi.php

1,000 lines 37.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Api\Checkout;
4
5 use FluentCart\Api\Resource\FrontendResource\CustomerAddressResource;
6 use FluentCart\Api\Resource\FrontendResource\CustomerResource;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\App\App;
9 use FluentCart\App\Events\Order\OrderCreated;
10 use FluentCart\App\Events\StockChanged;
11 use FluentCart\App\Helpers\AddressHelper;
12 use FluentCart\App\Helpers\CartCheckoutHelper;
13 use FluentCart\App\Helpers\CartHelper;
14 use FluentCart\App\Helpers\CheckoutProcessor;
15 use FluentCart\App\Helpers\Status;
16 use FluentCart\App\Helpers\UtmHelper;
17 use FluentCart\App\Models\Cart;
18 use FluentCart\App\Models\Customer;
19 use FluentCart\App\Models\CustomerAddresses;
20 use FluentCart\App\Models\Order;
21 use FluentCart\App\Models\OrderAddress;
22 use FluentCart\App\Models\ShippingMethod;
23 use FluentCart\App\Services\CheckoutService;
24 use FluentCart\App\Services\Localization\LocalizationManager;
25 use FluentCart\App\Services\OrderService;
26 use FluentCart\App\Services\Payments\PaymentHelper;
27 use FluentCart\App\Services\Payments\PaymentInstance;
28 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
29 use FluentCart\Framework\Http\Response;
30 use FluentCart\App\Services\RateLimiter;
31 use FluentCart\Framework\Support\Arr;
32 use FluentCart\Framework\Support\Str;
33 use FluentCart\Framework\Validator\Validator;
34
35 class CheckoutApi
36 {
37 /**
38 * @throws \Exception
39 */
40
41 public static function placeOrder(array $data, $fromCheckout = false)
42 {
43
44 RateLimiter::isSpamming('place_order_attempt', 5, 60, true);
45
46 $userTz = Arr::get($data, 'user_tz', 'UTC');
47
48 $cart = CartHelper::getCart();
49
50 if (!$cart || !$cart->cart_data || $cart->stage === 'completed') {
51 wp_send_json([
52 'status' => 'failed',
53 'message' => __('Cart is empty or already completed', 'fluent-cart'),
54 ]);
55 }
56
57 $cart = $cart->reValidateCoupons();
58
59 $cartData = $cart->cart_data;
60 $prevOrder = $cart->order;
61 if ($prevOrder) {
62 $prevOrder->load('order_items');
63 }
64 $isLockedCart = $cart->isLocked();
65
66 // todo: we should handle this logic as we have multiple options like PAYMENT_PARTIALLY_PAID....
67 if ($prevOrder &&
68 (
69 in_array($prevOrder->status, Status::getOrderSuccessStatuses()) ||
70 $prevOrder->payment_status != Status::PAYMENT_PENDING
71 )
72 ) {
73 wp_send_json([
74 'status' => 'failed',
75 'message' => __('You have already completed this order.', 'fluent-cart'),
76 ]);
77 }
78
79 $data = static::addLoggedUserData($data);
80
81
82 // Validate using filter hook (allows addons/modules to validate)
83 $validation = apply_filters('fluent_cart/checkout/validate_before_process', true, $data);
84
85 if (is_wp_error($validation)) {
86 wp_send_json([
87 'status' => 'failed',
88 'message' => $validation->get_error_message(),
89 ], 403);
90 }
91
92 if (empty($data['billing_address_id'])) {
93 if ($prevOrder instanceof Order) {
94 $oldCustomer = $prevOrder->customer;
95 if ($oldCustomer) {
96 $fallbackAddress = $oldCustomer->billing_address;
97 if ($fallbackAddress) {
98 $data['billing_address_id'] = $fallbackAddress->first()->id;
99 }
100 }
101 }
102 } else if ($prevOrder) {
103 $data['order_id'] = $prevOrder->id;
104 }
105
106 $data = static::prepareAddressData($data);
107
108 $cartCheckoutService = new CheckoutService($cartData);
109 $validatedData = static::validateData($data, $cart, $cartCheckoutService, $prevOrder);
110
111
112
113
114 if (is_wp_error($validatedData)) {
115 wp_send_json([
116 'status' => 'failed',
117 'errors' => $validatedData->get_error_data(),
118 ]);
119 }
120
121 if (!CheckoutFieldsSchema::isFullNameRequired()) {
122 if (!empty($validatedData['billing_full_name']) && empty($validatedData['billing_first_name'])) {
123 // Modal checkout sends billing_full_name — split into first/last name
124 $nameParts = explode(' ', $validatedData['billing_full_name'], 2);
125 $validatedData['billing_first_name'] = $nameParts[0];
126 $validatedData['billing_last_name'] = $nameParts[1] ?? '';
127 } else {
128 $validatedData['billing_full_name'] = trim(
129 Arr::get($validatedData, 'billing_first_name') . ' ' . Arr::get($validatedData, 'billing_last_name')
130 );
131 }
132 }
133
134 $orderData = OrderService::groupSanitizedData($validatedData);
135
136 $shippingMethodId = Arr::get($orderData, 'others.fc_shipping_method');
137
138 $shippingCharge = 0;
139 if (!$cartCheckoutService->isAllDigital()) {
140 $shippingMethod = ShippingMethod::query()->find($shippingMethodId);
141 if (!empty($shippingMethod)) {
142 $shippingCharge = CartHelper::calculateShippingMethodCharge($shippingMethod, $cartData);
143 }
144 }
145
146 Arr::set($orderData, 'others.shipping_total', $shippingCharge);
147
148 $cartCheckoutHelper = CartCheckoutHelper::make();
149
150 try {
151 OrderService::validateProducts($cartCheckoutHelper->getItems(), $prevOrder);
152 } catch (\Exception $e) {
153 wp_send_json([
154 'message' => $e->getMessage(),
155 ], 422);
156 }
157
158 $paymentMethod = PaymentHelper::validateAndGetPayMethod($cartCheckoutHelper, $orderData, $shippingCharge);
159
160 Arr::set($orderData, 'others.payment_method', $paymentMethod);
161
162 $orderData['user_tz'] = $userTz;
163
164 $customer = static::getOrCreateCustomer($cartCheckoutHelper, $orderData);
165
166 $shouldCreateUser = static::shouldCreateUser($orderData, Arr::get($orderData, 'billing_address', []));
167
168 $taxTotal = (int)Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); // behavoir not applied here
169
170 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
171 $taxBehavior = apply_filters('fluent_cart/cart/tax_behavior', 0, ['cart' => $cart]);
172
173 // Ensure cart has the current payment method before recalculating fees
174 $checkoutData = $cart->checkout_data ?? [];
175 $checkoutData['payment_method'] = $paymentMethod;
176 $cart->checkout_data = $checkoutData;
177
178 $cart->clearFeeCache();
179 $fees = $cart->getFees();
180
181 $checkoutProcessor = new CheckoutProcessor($cartCheckoutHelper->getItems(), [
182 'customer_id' => $customer->id,
183 'user_tz' => $userTz,
184 'create_account_after_paid' => $shouldCreateUser ? 'yes' : 'no',
185 'shipping_charge' => $shippingCharge,
186 'tax_total' => $taxTotal,
187 'tax_behavior' => $taxBehavior,
188 'shipping_tax' => $shippingTax,
189 'payment_method' => $paymentMethod,
190 'applied_coupons' => $cart->getDiscountLines(),
191 'billing_address' => Arr::get($orderData, 'billing_address', []),
192 'shipping_address' => Arr::get($orderData, 'shipping_address', []),
193 'cart_hash' => $cart->cart_hash,
194 'is_locked' => $isLockedCart,
195 'manual_discount_total' => $cartCheckoutHelper->getManualDiscountAmount(),
196 'ip_address' => AddressHelper::getIpAddress(),
197 'note' => Arr::get($orderData, 'others.order_notes', ''),
198 'tax_id' => Arr::get($validatedData, 'billing_tax_id', 0),
199 'fees' => $fees,
200 ]);
201
202 $createdOrder = $checkoutProcessor->createDraftOrder($prevOrder);
203 if (is_wp_error($createdOrder)) {
204 wp_send_json([
205 'status' => 'failed',
206 'message' => $createdOrder->get_error_message(),
207 'data' => $createdOrder->get_error_data()
208 ], 423);
209 }
210
211 do_action('fluent_cart/order/after_items_calculated', [
212 'order' => $createdOrder,
213 'cart' => $cart,
214 'cart_items' => $cartCheckoutHelper->getItems(),
215 ]);
216
217 // prepare other data if any module needs to add data to the order data
218 do_action('fluent_cart/checkout/prepare_other_data', [
219 'cart' => $cart,
220 'order' => $createdOrder,
221 'prev_order' => $prevOrder,
222 'request_data' => $data,
223 'validated_data' => $validatedData
224 ]);
225
226 static::finalizeOrder($createdOrder, [
227 'billing_address' => Arr::get($orderData, 'billing_address', []),
228 'shipping_address' => Arr::get($orderData, 'shipping_address', []),
229 'items' => $cartCheckoutHelper->getItems(),
230 'from_checkout' => true,
231 'prev_order' => $prevOrder
232 ]);
233 }
234
235 private static function getOrCreateCustomer(CartCheckoutHelper $cartCheckoutHelper, $orderData)
236 {
237 $customerEmail = static::getCustomerEmail($orderData['billing_address']);
238 if (is_user_logged_in()) {
239 $customerEmail = wp_get_current_user()->user_email;
240 Arr::set($orderData, 'billing_address.email', $customerEmail);
241 }
242 $customer = $cartCheckoutHelper->getCustomer($customerEmail);
243 return static::createCustomerWithAddress(
244 $customer,
245 $orderData,
246 $orderData['billing_address'],
247 $orderData['shipping_address']
248 );
249 }
250
251 private static function prepareAddressData($data)
252 {
253 $shippingAddressId = Arr::get($data, 'shipping_address_id');
254 $billingAddressId = Arr::get($data, 'billing_address_id');
255 $orderId = Arr::get($data, 'order_id', null);
256
257 $shipToDifferent = Arr::get($data, 'ship_to_different', 'no');
258
259 $datKeys = ['country', 'address_1', 'address_2', 'city', 'state', 'postcode', 'phone', 'label'];
260
261 if ($billingAddressId) {
262 $prevOrder = Order::query()->find($orderId);
263 $prevBillingId = null;
264 if ($prevOrder) {
265 $prevBillingAddress = $prevOrder->billing_address;
266 $prevBillingId = Arr::get($prevBillingAddress, 'id');
267 }
268
269 $billingAddress = null;
270 if ($orderId && $prevBillingId == $billingAddressId) {
271 $billingAddress = OrderAddress::query()
272 ->where('id', $billingAddressId)
273 ->where('type', 'billing')
274 ->first();
275 }
276 if (empty($billingAddress)) {
277 $billingAddress = CustomerAddresses::query()
278 ->where('id', $billingAddressId)
279 ->where('type', 'billing')
280 ->first();
281 }
282
283 if ($billingAddress) {
284 foreach ($datKeys as $key) {
285 // Guest user, take data from form
286 if (!is_user_logged_in()) {
287 $data['billing_' . $key] = Arr::get($data, 'billing_' . $key, '');
288 } else {
289 $data['billing_' . $key] = $billingAddress->{$key};
290 }
291 }
292 }
293 }
294
295 if ($shippingAddressId && $shipToDifferent === 'yes') {
296 $prevShippingOrder = Order::query()->find($orderId);
297 $prevShippingAddress = $prevShippingOrder ? $prevShippingOrder->shipping_address : null;
298 $prevShippingId = Arr::get($prevShippingAddress, 'id', null);
299 $shippingAddress = null;
300 if ($orderId && $prevShippingId == $shippingAddressId) {
301 $shippingAddress = OrderAddress::query()
302 ->where('id', $shippingAddressId)
303 ->where('type', 'shipping')
304 ->first();
305 }
306 if (empty($shippingAddress)) {
307 $shippingAddress = CustomerAddresses::query()
308 ->where('id', $shippingAddressId)
309 ->where('type', 'shipping')
310 ->first();
311 }
312
313 if ($shippingAddress) {
314 $data['shipping_full_name'] = $shippingAddress->name;
315 foreach ($datKeys as $key) {
316 // Guest user, take data from form
317 if (!is_user_logged_in()) {
318 $data['shipping_' . $key] = Arr::get($data, 'shipping_' . $key, '');
319 } else {
320 $data['shipping_' . $key] = $shippingAddress->{$key};
321 }
322 }
323 }
324 } else if ($shipToDifferent !== 'yes') {
325 // if not different shipping, copy billing to shipping
326 foreach ($datKeys as $key) {
327 $data['shipping_' . $key] = Arr::get($data, 'billing_' . $key, '');
328 }
329 $data['shipping_full_name'] = Arr::get($data, 'billing_full_name', '');
330 }
331
332 return $data;
333 }
334
335 private static function addLoggedUserData(array $data): array
336 {
337 if (is_user_logged_in()) {
338 $checkoutHelper = CartCheckoutHelper::make();
339 $data['billing_email'] = wp_get_current_user()->user_email;
340
341 if (CheckoutFieldsSchema::isFullNameRequired()) {
342 $userFullName = $checkoutHelper->getFullName();
343 if (!empty($userFullName) && empty($data['billing_full_name'])) {
344 $data['billing_full_name'] = $userFullName;
345 }
346 } else {
347 $firstName = $checkoutHelper->getFirstName();
348 if (!empty($firstName) && empty($data['billing_first_name'])) {
349 $data['billing_first_name'] = $firstName;
350 }
351
352 $lastName = $checkoutHelper->getLastName();
353 if (!empty($lastName) && empty($data['billing_last_name'])) {
354 $data['billing_last_name'] = $lastName;
355 }
356
357 }
358
359
360 }
361 return $data;
362 }
363
364 private static function finalizeOrder(Order $order, $args = [])
365 {
366 AddressHelper::insertOrderAddresses(
367 $order->id,
368 Arr::get($args, 'billing_address', []),
369 Arr::get($args, 'shipping_address', [])
370 );
371
372 static::syncCustomerNames($order, $args);
373 $cart = CartHelper::getCart();
374
375 $utmData = [];
376 if (!empty($cart) && is_array($cart->utm_data) && count($cart->utm_data) > 0) {
377 $utmData = $cart->utm_data;
378 }
379
380 $requestUtmData = UtmHelper::getUtmDataOfRequest();
381 $utmData = wp_parse_args($requestUtmData, $utmData);
382 UtmHelper::addUtmToOrder($order->id, $utmData);
383
384 $prevOrder = Arr::get($args, 'prev_order', null);
385
386 (new OrderCreated($order, $prevOrder, $order->customer, $order->getLatestTransaction()))->dispatch();
387
388 static::updateStock($order);
389
390 // we don't have to validate the payment method again, as it's already validated in placeOrder method
391 $gateway = App::gateway($order->payment_method);
392
393 if (!$gateway) {
394 wp_send_json([
395 'status' => 'failed',
396 'message' => __('Payment method not found!', 'fluent-cart'),
397 'data' => []
398 ], 404);
399 }
400
401 $paymentInstance = new PaymentInstance($order);
402
403 $data = $gateway->makePaymentFromPaymentInstance($paymentInstance);
404
405 if (is_wp_error($data)) {
406 wp_send_json([
407 'status' => 'failed',
408 'message' => $data->get_error_message(),
409 'data' => $data->get_error_data()
410 ], 422);
411 }
412
413 wp_send_json($data, 200);
414 }
415
416 private static function syncCustomerNames($order, $args)
417 {
418 $customer = $order->customer;
419
420 if (empty($customer)) {
421 return;
422 }
423
424 $firstName = Arr::get($args, 'billing_address.first_name');
425 $lastName = Arr::get($args, 'billing_address.last_name');
426
427 $customer->update([
428 'first_name' => $firstName,
429 'last_name' => $lastName,
430 ]);
431
432 $user = get_user_by('email', $customer->email);
433
434 if (empty($user)) {
435 return;
436 }
437
438 if (is_user_logged_in() && $user->ID === get_current_user_id()) {
439 update_user_meta($user->ID, 'first_name', $firstName);
440 update_user_meta($user->ID, 'last_name', $lastName);
441 }
442 }
443
444 public static function updateStock($order)
445 {
446 // pluck product ids to update product default variation as stock has changed
447 $productIds = OrderService::pluckProductIds($order);
448 if (!empty($productIds)) {
449 (new StockChanged($productIds))->dispatch();
450 }
451 }
452
453 public static function createCustomerWithAddress($customer, $orderData, $billingAddress, $shippingAddress)
454 {
455 if (empty($customer)) {
456 $customer = static::createNewCustomer($orderData, $billingAddress, $shippingAddress);
457 } else if ($customer instanceof Customer) {
458 static::updateExistingCustomer($customer, $orderData, $billingAddress, $shippingAddress);
459 }
460
461 return $customer;
462 }
463
464 private static function createNewCustomer($orderData, &$billingAddress, $shippingAddress)
465 {
466 global $current_user;
467 if ($current_user->ID) {
468 $billingAddress['email'] = $current_user->user_email;
469 $billingAddress['user_id'] = $current_user->ID;
470 } else {
471 static::handleUserCreation($orderData, $billingAddress);
472 }
473
474 $customer = CustomerResource::create($billingAddress);
475 $customer = Arr::get($customer, 'data', null);
476 $customerId = Arr::get($customer, 'id', null);
477 static::createCustomerAddress($billingAddress, $customerId);
478 static::createCustomerAddress($shippingAddress, $customerId);
479
480 return $customer;
481 }
482
483 private static function updateExistingCustomer($customer, $orderData, $billingAddress, $shippingAddress)
484 {
485 if (empty($customer->user_id)) {
486 $currentLoggedInUser = wp_get_current_user();
487 if ($currentLoggedInUser && $currentLoggedInUser->user_email === $customer->email) {
488 $userId = get_current_user_id();
489 $customer->update(['user_id' => $userId]);
490 $billingAddress['user_id'] = $userId;
491 }
492 }
493
494 $customer->load(['billing_address', 'shipping_address']);
495
496 if ($customer->billing_address->count() < 1) {
497 static::createCustomerAddress($billingAddress, $customer->id);
498 }
499 if ($customer->shipping_address->count() < 1) {
500 static::createCustomerAddress($shippingAddress, $customer->id);
501 }
502
503 static::handleUserCreation($orderData, $billingAddress, $customer);
504 }
505
506 private static function handleUserCreation($orderData, &$billingAddress, $customer = null)
507 {
508 $userEmail = Arr::get($billingAddress, 'email');
509 $user = get_user_by('email', $userEmail);
510
511 if ($user) {
512 $billingAddress['user_id'] = $user->ID;
513 if ($customer) {
514 $customer->update(['user_id' => $user->ID]);
515 }
516 }
517 }
518
519 private static function getCustomerEmail($billingAddress)
520 {
521 return is_user_logged_in() ? wp_get_current_user()->user_email : $billingAddress['email'];
522 }
523
524 public static function shouldCreateUser($data, $billingAddress): bool
525 {
526 $accountCreationMod = (new StoreSettings())->get('user_account_creation_mode');
527 $hasSubscription = (CartCheckoutHelper::make())->hasSubscription() === 'yes';
528 if ($accountCreationMod === 'all' || $hasSubscription) {
529 return true;
530 }
531
532 if (is_user_logged_in()) {
533 return false;
534 }
535
536 $allow_create_account = Arr::get($data, 'others.allow_create_account') === 'yes';
537
538 $userEmail = Arr::get($billingAddress, 'email');
539 $user = get_user_by('email', $userEmail);
540
541 return ($allow_create_account && $accountCreationMod === 'user_choice') || !empty($user);
542 }
543
544 private static function createCustomerAddress(array $address, $customerId)
545 {
546 CustomerAddressResource::create($address, ['id' => $customerId]);
547 }
548
549 /**
550 * Check if a customer is logged in and has the given address type.
551 *
552 * @param string $type Address type to check ('billing' or 'shipping').
553 * @return bool True if the customer is logged in and the address is available.
554 */
555 private static function hasCustomerWithAddress(string $type): bool
556 {
557 $addressType = $type . '_address';
558 $currentCustomer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
559
560 return !empty($currentCustomer) && $currentCustomer->$addressType->count() === 1;
561 }
562
563
564 /**
565 * Validate the data against the provided rules.
566 * @return array
567 */
568
569 public static function billingRules($data = []): array
570 {
571 $baseRules = [
572 'billing_full_name' => 'required|sanitizeText|maxLength:255',
573 'billing_email' => 'required|sanitizeText|email|maxLength:255',
574 'order_notes' => 'nullable|sanitizeTextArea|maxLength:200',
575 ];
576
577 return static::generateAddressRules('billing', $data, $baseRules, 'getBillingAddressFields');
578 }
579
580 public static function shippingIdRules(): array
581 {
582 return [
583 'shipping_address' => 'exists:fct_customer_addresses,id',
584 ];
585 }
586
587 public static function shippingRules($data = []): array
588 {
589 $baseRules = [
590 'shipping_full_name' => 'required|sanitizeText|maxLength:255'
591 ];
592
593 return static::generateAddressRules('shipping', $data, $baseRules, 'getShippingAddressFields');
594
595 }
596
597 /**
598 * Validate the data against the provided rules.
599 *
600 * @return false|\WP_Error|string
601 */
602
603 public static function validateData($data, Cart $cart, CheckoutService $cartCheckoutService, $prevOrder)
604 {
605 $shippingRequired = $cart->requireShipping();
606 $isDifferentShipping = $shippingRequired && Arr::get($data, 'ship_to_different', 'no') === 'yes';
607 $fulfillmentType = $shippingRequired ? 'physical' : 'digital';
608
609 $billingValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements('billing', $fulfillmentType, !$isDifferentShipping));
610
611 // Name fields are validated separately below (full_name/first_name/last_name)
612 unset($billingValidations['full_name'], $billingValidations['first_name'], $billingValidations['last_name'], $billingValidations['company_name']);
613
614 if (!isset($billingValidations['country'])) {
615 // get store country
616 $data['billing_country'] = (new StoreSettings())->get('store_country');
617 }
618
619 $billingAddress = [];
620 foreach ($billingValidations as $key => $billingValidation) {
621 $billingAddress[$key] = Arr::get($data, 'billing_' . $key, '');
622 }
623 if (!isset($billingAddress['country'])) {
624 // get store country
625 $billingAddress['country'] = (new StoreSettings())->get('store_country');
626 }
627
628 $shippingAddress = [];
629 $shippingValidations = [];
630 if ($isDifferentShipping) {
631 $shippingValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements('shipping', 'physical'));
632 // Name fields are validated via billing basic_info, not shipping address
633 unset($shippingValidations['full_name'], $shippingValidations['first_name'], $shippingValidations['last_name'], $shippingValidations['company_name']);
634 foreach ($shippingValidations as $key => $shippingValidation) {
635 $shippingAddress[$key] = Arr::get($data, 'shipping_' . $key, '');
636 }
637 }
638
639 if (Arr::get($data, 'ship_to_different', 'no') === 'yes') {
640 if (!isset($data['shipping_country'])) {
641 // get store country
642 $data['shipping_country'] = (new StoreSettings())->get('store_country');
643 }
644 if (!isset($shippingAddress['country'])) {
645 // get store country
646 $shippingAddress['country'] = (new StoreSettings())->get('store_country');
647 }
648
649 }
650
651 $errors = [];
652
653 $agreeTermsRequired = CheckoutFieldsSchema::isTermsRequired();
654
655 $customTitles = [
656 'address_1' => 'Street Address',
657 'address_2' => 'Apt, Suite, Unit',
658 ];
659
660 foreach ($billingValidations as $key => $rule) {
661 $value = Arr::get($billingAddress, $key, '');
662 $prefixedKey = 'billing_' . $key;
663 $titledKey = $customTitles[$key] ?? Str::headline($key);
664
665 if ($key === 'country') {
666 $countries = LocalizationManager::getInstance()->countries();
667
668 if ($rule === 'required' && empty($value)) {
669 if (!isset($errors[$key])) {
670 $errors[$prefixedKey] = [];
671 }
672 $errors[$prefixedKey]['required'] = sprintf(
673 /* translators: %s attribute name */
674 __('%s is required.', 'fluent-cart'),
675 $titledKey
676 );
677 continue;
678 }
679
680 if (!empty($value) && !Arr::has($countries, $value)) {
681 if (!isset($errors[$key])) {
682 $errors[$prefixedKey] = [];
683 }
684 $errors[$prefixedKey]['invalid'] = sprintf(
685 /* translators: %s attribute name */
686 __('%s is invalid.', 'fluent-cart'),
687 $titledKey
688 );
689 continue;
690 }
691
692 continue;
693 }
694
695 if ($key === 'state') {
696 $country = Arr::get($billingAddress, 'country', '');
697
698 $states = LocalizationManager::getInstance()->statesOptions($country);
699
700 if (empty($states)) {
701 continue;
702 }
703
704 if ($rule === 'required' && empty($value)) {
705 if (!isset($errors[$key])) {
706 $errors[$prefixedKey] = [];
707 }
708 $errors[$prefixedKey]['required'] = sprintf(
709 /* translators: %s attribute name */
710 __('%s is required.', 'fluent-cart'),
711 $titledKey
712 );
713 continue;
714 }
715
716 if (!empty($value) && !in_array($value, array_column($states, 'value'))) {
717 if (!isset($errors[$key])) {
718 $errors[$prefixedKey] = [];
719 }
720 $errors[$prefixedKey]['invalid'] = sprintf(
721 /* translators: %s attribute name */
722 __('%s is invalid.', 'fluent-cart'),
723 $titledKey
724 );
725 }
726
727 continue;
728 }
729
730 if ($rule === 'required' && empty($value)) {
731 if (!isset($errors[$key])) {
732 $errors[$prefixedKey] = [];
733 }
734 $errors[$prefixedKey]['required'] = sprintf(
735 /* translators: %s attribute name */
736 __('%s is required.', 'fluent-cart'),
737 $titledKey
738 );
739 }
740 }
741
742 foreach ($shippingValidations as $key => $rule) {
743 $value = Arr::get($shippingAddress, $key, '');
744 $prefixedKey = 'shipping_' . $key;
745 $titledKey = $customTitles[$key] ?? Str::headline($key);
746
747 if ($key === 'country') {
748 $countries = LocalizationManager::getInstance()->countries();
749
750 if ($rule === 'required' && empty($value)) {
751 if (!isset($errors[$key])) {
752 $errors[$prefixedKey] = [];
753 }
754 $errors[$prefixedKey]['required'] = sprintf(
755 /* translators: %s attribute name */
756 __('%s is required.', 'fluent-cart'),
757 $titledKey
758 );
759
760 continue;
761 }
762
763 if (!empty($value) && !Arr::has($countries, $value)) {
764 if (!isset($errors[$key])) {
765 $errors[$prefixedKey] = [];
766 }
767 $errors[$prefixedKey]['invalid'] = sprintf(
768 /* translators: %s attribute name */
769 __('%s is invalid.', 'fluent-cart'),
770 $titledKey
771 );
772 continue;
773 }
774
775 continue;
776 }
777
778 if ($key === 'state') {
779 $country = Arr::get($shippingAddress, 'country', '');
780
781 $states = LocalizationManager::getInstance()->statesOptions($country);
782
783 if (empty($states)) {
784 continue;
785 }
786
787 if ($rule === 'required' && empty($value)) {
788 if (!isset($errors[$key])) {
789 $errors[$prefixedKey] = [];
790 }
791 $errors[$prefixedKey]['required'] = sprintf(
792 /* translators: %s attribute name */
793 __('%s is required.', 'fluent-cart'),
794 $titledKey
795 );
796
797 continue;
798 }
799
800 if (!empty($value) && !in_array($value, array_column($states, 'value'))) {
801 if (!isset($errors[$key])) {
802 $errors[$prefixedKey] = [];
803 }
804 $errors[$prefixedKey]['invalid'] = sprintf(
805 /* translators: %s attribute name */
806 __('%s is invalid.', 'fluent-cart'),
807 $titledKey
808 );
809 }
810
811 continue;
812 }
813
814 if ($rule === 'required' && empty($value)) {
815 if (!isset($errors[$key])) {
816 $errors[$prefixedKey] = [];
817 }
818 $errors[$prefixedKey]['required'] = sprintf(
819 /* translators: %s attribute name */
820 __('%s is required.', 'fluent-cart'),
821 $titledKey
822 );
823 }
824 }
825
826 $basicInfoFields = (CheckoutFieldsSchema::getNameEmailFieldsSchema())['fields'];
827
828 foreach ($basicInfoFields as $field) {
829 $fieldName = (string)Arr::get($field, 'name', '');
830 $isRequired = Arr::get($field, 'required', 'no') === 'yes';
831 if ($fieldName && $isRequired) {
832 $value = Arr::get($data, $fieldName, '');
833 if (empty($value)) {
834 Arr::set($errors, $fieldName . '.required', sprintf(
835 /* translators: %s attribute name */
836 __('%s is required.', 'fluent-cart'),
837 Arr::get($field, 'aria-label')
838 ));
839 }
840 }
841 }
842
843 if (empty($data['agree_terms']) && $agreeTermsRequired) {
844 $errors['agree_terms']['required'] = __('You must agree to the terms and conditions.', 'fluent-cart');
845 }
846
847 if (empty($data['billing_email']) || !is_email($data['billing_email'])) {
848 $errors['billing_email']['invalid'] = __('Email must be a valid email address.', 'fluent-cart');
849 }
850
851 if (CheckoutFieldsSchema::isFullNameRequired() || !empty($data['billing_full_name'])) {
852 // Modal checkout always sends billing_full_name regardless of store name field settings
853 if (empty($data['billing_full_name'])) {
854 $errors['billing_full_name']['required'] = __('Full name is required.', 'fluent-cart');
855 }
856 } else {
857 if (empty($data['billing_first_name'])) {
858 $errors['billing_first_name']['required'] = __('First name is required.', 'fluent-cart');
859 }
860
861 if(CheckoutFieldsSchema::isLastNameRequired()) {
862 if (empty($data['billing_last_name'])) {
863 $errors['billing_last_name']['required'] = __('Last name is required.', 'fluent-cart');
864 }
865 }
866 }
867
868
869 if ($cart->requireShipping()) {
870 if (!empty($data['fc_selected_shipping_method'])) {
871 $selectedMethod = $data['fc_selected_shipping_method'];
872 $shippingCountry = Arr::get($data, 'billing_country', '');
873 $shippingState = Arr::get($data, 'billing_state', '');
874 $shipToDifferent = Arr::get($data, 'ship_to_different', 'no') === 'yes';
875
876 if ($shipToDifferent) {
877 $shippingCountry = Arr::get($data, 'shipping_country', '');
878 $shippingState = Arr::get($data, 'shipping_state', '');
879 }
880
881 $availableShippingMethods = AddressHelper::getShippingMethods($shippingCountry, $shippingState);
882
883
884 if (empty($availableShippingMethods) || is_wp_error($availableShippingMethods)) {
885 $errors['shipping_method']['unavailable'] = __('We don\'t ship to this address. Please select a different address.', 'fluent-cart');
886 } else {
887 $found = false;
888 foreach ($availableShippingMethods as $shippingMethod) {
889 if ($shippingMethod->id == $selectedMethod) {
890 $found = true;
891 break;
892 }
893 }
894
895 if (!$found) {
896 $errors['shipping_method']['invalid'] = __('The selected shipping method is not available.', 'fluent-cart');
897 }
898 }
899
900
901 } else {
902 $errors['shipping_method']['required'] = __('You must select a shipping method.', 'fluent-cart');
903 }
904 }
905
906 $errors = apply_filters('fluent_cart/checkout/validate_data', $errors, [
907 'data' => $data,
908 'cart' => $cart
909 ]);
910
911 if (count($errors) > 0) {
912 return new \Wp_Error('validation_error', 'Validation error', $errors);
913 }
914
915 return $data;
916 }
917
918 public static function validateShippingMethod(array $data, CheckoutService $cartCheckoutService): bool
919 {
920
921 if ($cartCheckoutService->isAllDigital()) {
922 return true;
923 }
924
925 $shipping_country = Arr::get($data, 'shipping_country');
926 $shipping_state = Arr::get($data, 'shipping_state');
927
928
929 $methods = ShippingMethod::getApplicableForCountry($shipping_country, $shipping_state)->keyBy('id');
930 if ($methods->count() === 0 || \FluentCart\App\App::isDevMode()) {
931 return true;
932 }
933
934 $shipping_method = Arr::get($data, 'fc_selected_shipping_method');
935
936 if (empty($shipping_method)) {
937 return false;
938 }
939
940
941 $exist = $methods->has($shipping_method);
942 if (!$exist) {
943 return false;
944 }
945
946 return true;
947 }
948
949
950 public static function messages(): array
951 {
952 return [
953 'billing_full_name.required' => esc_html__('Full name field is required.', 'fluent-cart'),
954 'billing_email.required' => esc_html__('Email field is required.', 'fluent-cart'),
955 'billing_email.email' => esc_html__('Email must be a valid email address.', 'fluent-cart'),
956 'billing_address.required' => esc_html__('Address field is required.', 'fluent-cart'),
957 'billing_country.required' => esc_html__('Country field is required.', 'fluent-cart'),
958 'billing_address_1.required' => esc_html__('Street Address field is required.', 'fluent-cart'),
959 'billing_city.required' => esc_html__('City field is required.', 'fluent-cart'),
960 'billing_postcode.required' => esc_html__('Postcode field is required.', 'fluent-cart'),
961 'shipping_full_name.required' => esc_html__('Full name field is required.', 'fluent-cart'),
962 // 'shipping_email.required' => esc_html__('Email field is required.', 'fluent-cart'),
963 // 'shipping_email.email' => esc_html__('Email must be a valid email address.', 'fluent-cart'),
964 'shipping_address.required' => esc_html__('Address field is required.', 'fluent-cart'),
965 'shipping_city.required' => esc_html__('City field is required.', 'fluent-cart'),
966 'shipping_postcode.required' => esc_html__('Postcode field is required.', 'fluent-cart'),
967 ];
968 }
969
970 private static function generateAddressRules($type, $data, $baseRules, $fieldGetter): array
971 {
972 $hasAddress = static::hasCustomerWithAddress($type);
973 $rules = App::localization()->getValidationRule($data, $type);
974
975 if ($hasAddress) {
976 $baseRules["{$type}_address"] = 'required|numeric';
977 return $baseRules;
978 }
979
980 $cartCheckoutHelper = CartCheckoutHelper::make();
981 $fields = $cartCheckoutHelper->{$fieldGetter}();
982 $addressFields = Arr::get($fields, 'address_section.schema', []);
983 $addressFields = Arr::wrap($addressFields);
984
985 $validFields = array_keys($addressFields);
986
987 // Add prefix to each field (billing_ or shipping_)
988 $validFields = array_map(function ($field) use ($type) {
989 return $type . '_' . $field;
990 }, $validFields);
991
992 // Filter only relevant rules
993 $filteredRules = array_filter($rules, function ($key) use ($validFields) {
994 return in_array($key, $validFields);
995 }, ARRAY_FILTER_USE_KEY);
996
997 return $baseRules + $filteredRules;
998 }
999 }
1000