PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
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
fluent-cart / app / Hooks / Cart / WebCheckoutHandler.php

WebCheckoutHandler.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.5.1, at app/Hooks/Cart/WebCheckoutHandler.php

1,230 lines 46.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Hooks\Cart;
4
5 use FluentCart\Api\Checkout\CheckoutApi;
6 use FluentCart\Api\PaymentMethods;
7 use FluentCart\Api\Resource\FrontendResource\CartResource;
8 use FluentCart\Api\Resource\CustomerResource;
9 use FluentCart\Api\Resource\FrontendResource\CustomerAddressResource;
10 use FluentCart\App\App;
11 use FluentCart\App\Helpers\AddressHelper;
12 use FluentCart\App\Helpers\CartCheckoutHelper;
13 use FluentCart\App\Helpers\CartHelper;
14 use FluentCart\App\Helpers\Helper;
15 use FluentCart\App\Helpers\UtmHelper;
16 use FluentCart\App\Models\Product;
17 use FluentCart\App\Models\ProductVariation;
18 use FluentCart\App\Models\ShippingMethod;
19 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
20 use FluentCart\App\Modules\Tax\TaxModule;
21 use FluentCart\App\Services\Localization\LocalizationManager;
22 use FluentCart\App\Services\Renderer\AddressSelectRenderer;
23 use FluentCart\App\Services\Renderer\CartDrawerRenderer;
24 use FluentCart\App\Services\Renderer\CartRenderer;
25 use FluentCart\App\Services\Renderer\CartSummaryRender;
26 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
27 use FluentCart\App\Services\Renderer\CheckoutRenderer;
28 use FluentCart\App\Services\Renderer\ProductModalRenderer;
29 use FluentCart\App\Services\Renderer\ShippingMethodsRender;
30 use FluentCart\Framework\Support\Arr;
31 use FluentCart\App\Services\Renderer\ModalCheckoutRenderer;
32
33 class WebCheckoutHandler
34 {
35 public function register()
36 {
37 add_action('wp_ajax_fluent_cart_place_order', [$this, 'handlePlaceOrderAjax']);
38 add_action('wp_ajax_nopriv_fluent_cart_place_order', [$this, 'handlePlaceOrderAjax']);
39
40 add_action('wp_ajax_fluent_cart_checkout_routes', [$this, 'globalCheckoutRouteHandler']);
41 add_action('wp_ajax_nopriv_fluent_cart_checkout_routes', [$this, 'globalCheckoutRouteHandler']);
42
43 add_action('fluent_cart/order_bump_succeed', function ($data) {
44 $cart = Arr::get($data, 'cart', null);
45 $order = Arr::get($data, 'order', null);
46
47 if (!$cart || !$order) {
48 return;
49 }
50
51 $order->addLog('Order Bump Succeeded', 'Order Bump done from variation ID: ' . Arr::get($cart->checkout_data, 'order_bump.upgraded_from', '') . ' to variation ID: ' . Arr::get($cart->checkout_data, 'order_bump.upgraded_to', ''));
52 $order->updateMeta('_order_bump', [
53 'upgraded_from' => Arr::get($cart->checkout_data, 'order_bump.upgraded_from', ''),
54 'upgraded_to' => Arr::get($cart->checkout_data, 'order_bump.upgraded_to', '')
55 ]);
56
57 });
58
59 }
60
61 public function globalCheckoutRouteHandler()
62 {
63 nocache_headers();
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
71 $startedAt = microtime(true);
72
73 $action = App::request()->get('fc_checkout_action');
74 $result = [];
75 switch ($action) {
76 case 'apply_coupon':
77 $result = $this->handleApplyCouponAjax();
78 break;
79 case 'remove_coupon':
80 $result = $this->handleRemoveCouponAjax();
81 break;
82 case 'get_checkout_summary_view':
83 case 'reapply_coupon':
84 $result = $this->handleGetCheckoutSummaryViewAjax();
85 break;
86 case 'get_order_info':
87 $result = $this->handleGetOrderInfoAjax();
88 break;
89 case 'save_checkout_data':
90 $result = $this->patchCheckoutData();
91 break;
92 case 'get_country_info':
93 $result = $this->handleGetCountryInfoAjax();
94 break;
95 case 'update_address_select':
96 $result = $this->handleUpdateAddressSelectAjax();
97 break;
98
99 case 'get_shipping_methods_list_view':
100 $result = $this->handleGetShippingMethodsListViewAjax();
101 break;
102 case 'fluent_cart_cart_update':
103 $result = $this->handleCartUpdateAjax();
104 break;
105 case 'fluent_cart_cart_status':
106 $result = $this->handleCartStatusAjax();
107 break;
108 case 'apply_order_bump':
109 $result = $this->handleOrderBumpRequest();
110 break;
111 case 'get_product_modal_view':
112 $result = $this->getProductModalView();
113 break;
114 }
115
116 if (is_wp_error($result)) {
117 wp_send_json([
118 'message' => Arr::get($result, 'message') ?? $result->get_error_message()
119 ], 422);
120 }
121
122 if (is_array($result)) {
123 $result['_bench'] = microtime(true) - $startedAt;
124 }
125
126 wp_send_json($result, 200);
127 }
128
129 public function handlePlaceOrderAjax()
130 {
131 nocache_headers();
132 $data = App::request()->all();
133
134 CheckoutApi::placeOrder($data, true);
135 }
136
137 public function getShippingChargeData($cart): array
138 {
139 $shippingMethodId = App::request()->getSafe('shipping_method_id', 'sanitize_text_field');
140
141 if (!$shippingMethodId) {
142 $shippingMethodId = Arr::get($cart->checkout_data, 'shipping_data.shipping_method_id');
143 }
144
145 $shippingMethod = ShippingMethod::query()->find($shippingMethodId);
146
147 $charge = 0;
148 if (!empty($shippingMethod)) {
149 $shippingCountry = Arr::get($cart->checkout_data, 'form_data.shipping_country');
150 $shippingState = Arr::get($cart->checkout_data, 'form_data.shipping_state');
151
152 $lists = AddressHelper::getAvailableShippingMethodLists(['country_code' => $shippingCountry, 'state' => $shippingState]);
153
154 if (isset($lists['available_shipping_methods'])) {
155 $availableShippingMethods = Arr::get($lists, 'available_shipping_methods', []);
156 $shippingMethodIds = Arr::pluck($availableShippingMethods, 'id');
157
158 if (in_array($shippingMethodId, $shippingMethodIds)) {
159 $shippingMethod = ShippingMethod::query()->find($shippingMethodId);
160 if (!empty($shippingMethod)) {
161 $charge = CartHelper::calculateShippingMethodCharge($shippingMethod);
162 }
163 } else {
164 CartHelper::resetShippingCharge();
165 }
166 } else {
167 CartHelper::resetShippingCharge();
168 }
169 }
170
171 return [
172 'charge' => $charge,
173 'formatted_charge' => Helper::toDecimal($charge),
174 'shippingMethodId' => $shippingMethodId
175 ];
176 }
177
178 public function handleGetOrderInfoAjax()
179 {
180 $method = App::request()->getSafe('method', 'sanitize_text_field');
181 $paymentManager = GatewayManager::getInstance()->get($method);
182 return $paymentManager->getOrderInfo(App::request()->all());
183 }
184
185 public function handleApplyCouponAjax()
186 {
187
188 $couponCode = App::request()->getSafe('coupon_code', 'sanitize_text_field');
189 $cart = CartHelper::getCart(null, false);
190
191 if (!$cart) {
192 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
193 }
194
195 $result = $cart->applyCoupon($couponCode);
196
197 if (is_wp_error($result)) {
198 return $result;
199 }
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
227 ob_start();
228 (new CartSummaryRender($cart))->render($withWrapper = false);
229 $summary = ob_get_clean();
230
231 $cartTotal = $cart->getEstimatedTotal();
232
233 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
234 $enableModalCheckout = App::request()->get('modal_checkout');
235
236 $fragments = [];
237
238 $fragments[] = [
239 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
240 'content' => $summary,
241 'type' => 'replace'
242 ];
243
244 if ($enableModalCheckout === 'yes') {
245 $fragments[] = [
246 'selector' => '[data-fluent-cart-checkout-payment-methods]',
247 'content' => $modalCheckoutRender->getFragment('payment_methods'),
248 'type' => 'replace'
249 ];
250 } else {
251 $fragments[] = [
252 'selector' => '[data-fluent-cart-checkout-payment-methods]',
253 'content' => (new CheckoutRenderer($cart))->getFragment('payment_methods'),
254 'type' => 'replace'
255 ];
256 }
257
258 $fragments[] = [
259 'selector' => '[data-fct-modal-checkout-summary-group]',
260 'content' => $modalCheckoutRender->getFragment('summary_group'),
261 'type' => 'replace'
262 ];
263
264 $fragments[] = [
265 'selector' => '[data-fct-modal-checkout-summary]',
266 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
267 'type' => 'replace'
268 ];
269
270 return [
271 'fragments' => $fragments,
272 'cart' => $cart,
273 'total' => $cartTotal,
274 'formatted_total' => Helper::toDecimal($cartTotal),
275 'applied_coupons' => $cart->coupons,
276 'has_subscriptions' => $cart->hasSubscription()
277 ];
278 }
279
280 public function handleRemoveCouponAjax()
281 {
282 $couponCode = App::request()->getSafe('coupon_code', 'sanitize_text_field');
283 $cart = CartHelper::getCart(null, false);
284
285 if (!$cart) {
286 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
287 }
288
289 $cart = $cart->removeCoupon($couponCode);
290
291 if (is_wp_error($cart)) {
292 return $cart;
293 }
294
295 ob_start();
296 (new CartSummaryRender($cart))->render($withWrapper = false);
297 $summary = ob_get_clean();
298
299 $cartTotal = $cart->getEstimatedTotal();
300
301 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
302 $enableModalCheckout = App::request()->get('modal_checkout');
303
304 $fragments = [];
305
306 $fragments[] = [
307 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
308 'content' => $summary,
309 'type' => 'replace'
310 ];
311
312 if ($enableModalCheckout === 'yes') {
313 $fragments[] = [
314 'selector' => '[data-fluent-cart-checkout-payment-methods]',
315 'content' => $modalCheckoutRender->getFragment('payment_methods'),
316 'type' => 'replace'
317 ];
318 } else {
319 $fragments[] = [
320 'selector' => '[data-fluent-cart-checkout-payment-methods]',
321 'content' => (new CheckoutRenderer($cart))->getFragment('payment_methods'),
322 'type' => 'replace'
323 ];
324 }
325
326 $fragments[] = [
327 'selector' => '[data-fct-modal-checkout-summary-group]',
328 'content' => $modalCheckoutRender->getFragment('summary_group'),
329 'type' => 'replace'
330 ];
331
332 $fragments[] = [
333 'selector' => '[data-fct-modal-checkout-summary]',
334 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
335 'type' => 'replace'
336 ];
337
338
339 return [
340 'fragments' => $fragments,
341 'total' => $cartTotal,
342 'formatted_total' => Helper::toDecimal($cartTotal),
343 'applied_coupons' => $cart->coupons,
344 'has_subscriptions' => $cart->hasSubscription()
345 ];
346 }
347
348 public function handleGetCheckoutSummaryViewAjax()
349 {
350 $cart = CartHelper::getCart(null, false);
351 if (!$cart) {
352 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
353 }
354
355 $cart = $cart->reValidateCoupons();
356
357 $checkOutHelper = CartCheckoutHelper::make();
358 $checkOutHelper->setCart($cart);
359
360 $shippingChargeData = $this->getShippingChargeData($cart);
361 $shippingCharge = Arr::get($shippingChargeData, 'charge');
362 $shippingMethodId = Arr::get($shippingChargeData, 'shippingMethodId');
363 $formattedShippingCharge = Arr::get($shippingChargeData, 'formatted_charge');
364 $subtotal = $checkOutHelper->getItemsAmountSubtotal(false);
365
366 $formattedSubtotal = $checkOutHelper->getItemsAmountSubtotal(true);
367
368 $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge;
369
370 // Include fee total
371 $feeTotal = $cart->getFeeTotal();
372 if ($feeTotal > 0) {
373 $totalPrice += $feeTotal;
374 }
375
376 if (!empty($cart->checkout_data['tax_data'])) {
377 $taxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
378 $totalPrice += $taxTotal;
379 }
380
381 $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
382
383 if (!empty($shippingCharge)) {
384 $cart->checkout_data = array_merge($cart->checkout_data, [
385 'shipping_data' => [
386 'shipping_method_id' => $shippingMethodId,
387 'shipping_charge' => $shippingCharge
388 ]
389 ]);
390 } else {
391 $cart->checkout_data = array_merge($cart->checkout_data, [
392 'shipping_data' => [
393 'shipping_method_id' => null,
394 'shipping_charge' => 0
395 ]
396 ]);
397 }
398
399 $cart->save();
400
401 // shipping charge changed
402 if ($shippingCharge !== $oldShippingCharge) {
403 do_action('fluent_cart/checkout/shipping_data_changed', [
404 'cart' => $cart
405 ]);
406 }
407
408
409 $totalPrice = apply_filters('fluent_cart/cart/estimated_total', $totalPrice, [
410 'cart' => $cart
411 ]);
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
419 $fragments = [];
420
421 $cartRender = (new CheckoutRenderer($cart));
422 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
423 $enableModalCheckout = App::request()->getSafe('modal_checkout', 'sanitize_text_field');
424
425 ob_start();
426 (new CartSummaryRender($cart))->render(false);
427 $cartSummaryInner = ob_get_clean();
428
429 $fragments[] = [
430 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
431 'content' => $cartSummaryInner,
432 'type' => 'replace'
433 ];
434
435 if ($enableModalCheckout === 'yes') {
436 $fragments[] = [
437 'selector' => '[data-fluent-cart-checkout-payment-methods]',
438 'content' => $modalCheckoutRender->getFragment('payment_methods'),
439 'type' => 'replace'
440 ];
441 } else {
442 $fragments[] = [
443 'selector' => '[data-fluent-cart-checkout-payment-methods]',
444 'content' => $cartRender->getFragment('payment_methods'),
445 'type' => 'replace'
446 ];
447 }
448
449 return [
450 'fragments' => $fragments,
451 'total' => $totalPrice,
452 'applied_coupons' => $cart->coupons,
453 'shipping_charge' => $shippingCharge,
454 'formatted_shipping_charge' => $formattedShippingCharge,
455 'has_subscriptions' => $cart->hasSubscription(),
456 'shipping_method_id' => $shippingMethodId
457 ];
458
459 }
460
461 public function handleUpdateAddressSelectAjax()
462 {
463 $customerAddressId = App::request()->get('customer_address_id');
464 $address = CustomerAddressResource::find($customerAddressId, ['with' => App::request()->get('with', [])]);
465
466 if (!$address) {
467 return [
468 'message' => __('Address not found', 'fluent-cart')
469 ];
470 }
471
472 //update address into cart
473 $addressId = Arr::get($address, 'address.id');
474 $country = Arr::get($address, 'address.country');
475 $state = Arr::get($address, 'address.state');
476 $type = Arr::get($address, 'address.type', 'billing');
477 $cart = CartHelper::getCart(App::request()->get('fct_cart_hash'));
478
479 $oldTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
480 $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
481
482 $previousFormData = Arr::get($cart->checkout_data, 'form_data', []);
483 $checkoutData = Arr::wrap($cart->checkout_data);
484 Arr::set($checkoutData, 'form_data.' . $type . '_address_id', $addressId);
485 Arr::set($checkoutData, 'form_data.' . $type . '_country', $country);
486 Arr::set($checkoutData, 'form_data.' . $type . '_state', $state);
487
488 if ($type === 'billing' && Arr::get($checkoutData, 'form_data.ship_to_different', 'no') === 'no') {
489 Arr::set($checkoutData, 'form_data.shipping_address_id', $addressId);
490 Arr::set($checkoutData, 'form_data.shipping_country', $country);
491 Arr::set($checkoutData, 'form_data.shipping_state', $state);
492 }
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
500 $cart->checkout_data = $checkoutData;
501
502 $cart->save();
503
504 $customerId = Arr::get($address, 'address.customer_id');
505
506 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
507 if (empty($customer) || $customer->id != $customerId) {
508 return [
509 'message' => __('You are not authorized to view this address', 'fluent-cart')
510 ];
511 }
512
513 $formattedAddress = Arr::get($address, 'address.formatted_address');
514
515 // Use output buffering to generate HTML
516 ob_start();
517
518 // Extract the address parts
519 $addressParts = [
520 trim(Arr::get($formattedAddress, 'address_1') ?? ''),
521 trim(Arr::get($formattedAddress, 'address_2') ?? ''),
522 trim(Arr::get($formattedAddress, 'city') ?? ''),
523 trim(Arr::get($formattedAddress, 'state') ?? ''),
524 trim(Arr::get($formattedAddress, 'country') ?? ''),
525 ];
526
527 // Filter out empty or null parts
528 $addressParts = array_filter($addressParts, function ($part) {
529 return $part !== '';
530 });
531
532 // Join parts with comma and space
533
534
535 do_action('fluent_cart/checkout/form_data_changed', [
536 'cart' => $cart
537 ]);
538
539 ob_start();
540 (new CartSummaryRender($cart))->render(false);
541 $cartSummaryInner = ob_get_clean();
542
543 $newTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
544 $newShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0);
545
546 $checkoutData = [
547 'message' => __('Address Attached', 'fluent-cart'),
548 'fragments' => [
549 [
550 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
551 'content' => $cartSummaryInner,
552 'type' => 'replace'
553 ]
554 ],
555 'tax_total_changes' => $oldTaxTotal != $newTaxTotal,
556 'shipping_charge_changes' => $oldShippingCharge != $newShippingCharge
557 ];
558
559 return apply_filters('fluent_cart/checkout/checkout_data_changed', $checkoutData, ['cart' => $cart]);
560
561 }
562
563 public function handleGetCountryInfoAjax()
564 {
565 $timezone = App::request()->getSafe('timezone', 'sanitize_text_field');
566
567 $code = App::request()->getSafe('country_code', 'sanitize_text_field');
568 $countryInfo = LocalizationManager::getCountryInfoFromRequest($timezone, $code);
569
570 return [
571 'country_info' => $countryInfo
572 ];
573 }
574
575 public function getShippingMethodsListView(array $data)
576 {
577 $availableShippingMethods = AddressHelper::getAvailableShippingMethodLists($data);
578 $shippingMethods = Arr::get($availableShippingMethods, 'available_shipping_methods');
579 $countryCode = Arr::get($availableShippingMethods, 'country_code');
580 $status = Arr::get($availableShippingMethods, 'status');
581
582 if ($status === false) {
583 return false;
584 }
585
586 $cart = CartHelper::getCart();
587
588 $cartRender = (new CheckoutRenderer($cart));
589
590 ob_start();
591 $cartRender->getFragment('shipping_methods');
592 $content = ob_get_clean();
593
594 return [
595 'view' => $content,
596 'country_code' => $countryCode,
597 'shipping_methods' => $shippingMethods
598 ];
599 }
600
601 public function handleGetShippingMethodsListViewAjax()
602 {
603 $data = [
604 'country_code' => App::request()->get('country_code'),
605 'state' => App::request()->get('state'),
606 'timezone' => App::request()->get('timezone')
607 ];
608
609 $cart = CartHelper::getCart();
610
611 if (!$cart) {
612 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
613 }
614
615 $availableShippingMethods = AddressHelper::getShippingMethods($data['country_code'], $data['state'], $data['timezone']);
616
617 ob_start();
618
619 $selectedId = Arr::get($cart->checkout_data, 'shipping_data.shipping_method_id', '');
620
621 $selectedId = CartHelper::resolveAutoSelectShippingMethod($cart, $availableShippingMethods ?: [], $selectedId);
622
623 if (!$availableShippingMethods || is_wp_error($availableShippingMethods)) {
624 (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render();
625 } else {
626 foreach ($availableShippingMethods as $method) {
627 $method->charge_amount = CartHelper::calculateShippingMethodCharge($method, $cart->cart_data);
628 }
629
630 (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render();
631 }
632
633 $shippingMethodsView = ob_get_clean();
634
635 return [
636 'status' => true,
637 'fragments' => [
638 [
639 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]',
640 'content' => $shippingMethodsView,
641 'type' => 'replace'
642 ]
643 ],
644 'country_code' => $data['country_code'],
645 'shipping_method_id' => $selectedId ?: null,
646 ];
647 }
648
649 public function handleCartStatusAjax()
650 {
651 return CartResource::getStatus();
652 }
653
654 public function handleCartUpdateAjax()
655 {
656 $requestData = App::request()->all();
657
658 $data = [
659 'item_id' => (int)Arr::get($requestData, 'item_id'),
660 'quantity' => (int)Arr::get($requestData, 'quantity', 0),
661 'by_input' => Arr::get($requestData, 'by_input', false),
662 'is_custom' => Arr::get($requestData, 'is_custom', false)
663 ];
664
665 $cart = CartResource::update($data, '', $requestData);
666
667 if (is_wp_error($cart)) {
668 return $cart;
669 }
670
671 do_action('fluent_cart/checkout/cart_amount_updated', [
672 'cart' => $cart
673 ]);
674
675 $itemCount = 0;
676
677 if ($cart) {
678 $itemCount = count($cart->cart_data ?? []);
679 }
680
681 $cartItems = Arr::get(CartResource::getStatus(), 'cart_data', []);
682
683 $defaultOpen = Arr::get($requestData, 'open_cart', false);
684 $isAdminBarEnabled = Arr::get($requestData, 'is_admin_bar_enabled', false);
685
686 $fragments = [];
687 if (!empty($cartItems)) {
688 ob_start();
689 (new CartDrawerRenderer($cartItems, [
690 'item_count' => $itemCount,
691 'open_cart' => $defaultOpen,
692 'is_admin_bar_enabled' => $isAdminBarEnabled
693 ]))->render();
694 $cartDrawerView = ob_get_clean();
695
696 ob_start();
697 (new CartRenderer($cartItems))->renderItems($cartItems);
698 $cartDrawerItemsView = ob_get_clean();
699
700 ob_start();
701 (new CartRenderer($cartItems))->renderTotal();
702 $cartDrawerItemsTotalView = ob_get_clean();
703
704 ob_start();
705 (new CartDrawerRenderer($cartItems, [
706 'item_count' => $itemCount,
707 'open_cart' => $defaultOpen,
708 'is_admin_bar_enabled' => $isAdminBarEnabled
709 ]))->renderItemCount();
710 $cartItemCount = ob_get_clean();
711
712 $fragments = [
713 [
714 'selector' => '[data-fluent-cart-cart-drawer-container]',
715 'content' => $cartDrawerView,
716 'type' => 'replace'
717 ],
718 [
719 'selector' => '[data-fluent-cart-cart-content-wrapper]',
720 'content' => $cartDrawerItemsView,
721 'type' => 'replace'
722 ],
723 [
724 'selector' => '[data-fluent-cart-cart-total-wrapper]',
725 'content' => $cartDrawerItemsTotalView,
726 'type' => 'replace'
727 ],
728 [
729 'selector' => '.fluent-cart-cart-badge-count',
730 'content' => $itemCount > 0 ? $cartItemCount : '',
731 'type' => 'replace'
732 ]
733 ];
734 }
735
736 if (empty($cartItems)) {
737 ob_start();
738 (new CartRenderer($cartItems))->renderEmpty();
739 $cartDrawerItemsView = ob_get_clean();
740 $fragments[] = [
741 'selector' => '[data-fluent-cart-cart-content-wrapper]',
742 'content' => $cartDrawerItemsView,
743 'type' => 'replace'
744 ];
745 }
746
747
748 return [
749 'message' => __('Cart updated successfully', 'fluent-cart'),
750 'data' => apply_filters('fluent_cart/checkout/cart_updated', [
751 'cart' => $cart,
752 ]),
753 'fragments' => $fragments
754 ];
755 }
756
757 public function patchCheckoutData()
758 {
759 $cart = CartHelper::getCart();
760 if (!$cart) {
761 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
762 }
763
764 $allData = App::request()->all();
765 $dataKey = sanitize_text_field((string) Arr::get($allData, 'data_key'));
766 $dataValue = sanitize_text_field((string) Arr::get($allData, 'data_value'));
767
768 if ($dataKey) {
769 $allData[$dataKey] = $dataValue;
770 }
771
772 $allData = AddressHelper::maybePushAddressDataForCheckout($allData, 'billing');
773
774 if (Arr::get($allData, 'ship_to_different') === 'yes') {
775 $allData = AddressHelper::maybePushAddressDataForCheckout($allData, 'shipping');
776 } else {
777 $allData = AddressHelper::mergeBillingWithShipping($allData);
778 }
779
780 $validKeys = [
781 'ship_to_different' => 'form_data.ship_to_different',
782 'billing_email' => 'form_data.billing_email',
783 'billing_address_id' => 'form_data.billing_address_id',
784 'shipping_address_id' => 'form_data.shipping_address_id',
785 'billing_company' => 'form_data.billing_company',
786 'order_notes' => 'form_data.order_notes',
787 'shipping_method_id' => 'shipping_data.shipping_method_id',
788 '_fct_pay_method' => 'form_data._fct_pay_method',
789 'billing_company_name' => 'form_data.billing_company_name',
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',
793 'fct_vat_declaration_note' => 'tax_data.declaration_note',
794 ];
795
796 $addressFieldKeys = [
797 'full_name',
798 'country',
799 'address_1',
800 'address_2',
801 'state',
802 'city',
803 'postcode',
804 'company_name',
805 'phone'
806 ];
807
808 foreach ($addressFieldKeys as $addressFieldKey) {
809 $validKeys['billing_' . $addressFieldKey] = 'form_data.billing_' . $addressFieldKey;
810 $validKeys['shipping_' . $addressFieldKey] = 'form_data.shipping_' . $addressFieldKey;
811 }
812
813 $prevFlatData = [];
814 $prevCheckoutData = $cart->checkout_data;
815 foreach ($validKeys as $dataName => $dataPath) {
816 $prevFlatData[$dataName] = Arr::get($prevCheckoutData, $dataPath, null);
817 }
818 // $prevFlatData = array_filter($prevFlatData);
819 $prevFlatData = array_filter($prevFlatData, function ($value) {
820 return $value !== null;
821 });
822 $validData = Arr::only($allData, array_keys($validKeys));
823
824
825 $normalizeData = [];
826
827 foreach ($validData as $key => $value) {
828 $prevValue = Arr::get($prevFlatData, $key, null);
829 if ($prevValue != $value) {
830 $normalizeData[$key] = $value;
831 }
832 }
833
834 // Force-check the explicit data_key field — bulk POST may have already stored it as a side-effect.
835 if ($dataKey && isset($validKeys[$dataKey]) && !isset($normalizeData[$dataKey])) {
836 $storedValue = (string) Arr::get($prevCheckoutData, $validKeys[$dataKey], '');
837 if ($storedValue !== $dataValue) {
838 $normalizeData[$dataKey] = $dataValue;
839 }
840 }
841
842 if (!$normalizeData) {
843 return [
844 'message' => __('No changes detected', 'fluent-cart')
845 ];
846 }
847
848 $normalizeData = $this->normalizeCheckoutChangeData($normalizeData, $allData);
849
850 // Validate these data and filter out invalid data points and also push the
851 // address data into this array.
852
853
854 $checkoutData = $cart->checkout_data;
855 foreach ($normalizeData as $normalizeKey => $normalizeValue) {
856 Arr::set($checkoutData, $validKeys[$normalizeKey], $normalizeValue);
857 }
858
859 $oldTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
860 $fillData = [
861 'checkout_data' => $checkoutData,
862 'cart_data' => $cart->cart_data,
863 'hook_changes' => [
864 'shipping' => false,
865 'tax' => false
866 ]
867 ];
868
869
870 $fillData = apply_filters('fluent_cart/checkout/before_patch_checkout_data', $fillData, [
871 'cart' => $cart,
872 'prev_data' => $prevFlatData,
873 'changes' => $normalizeData,
874 'all_data' => $allData
875 ]);
876
877 $hookChanges = Arr::get($fillData, 'hook_changes', []);
878 unset($fillData['hook_changes']);
879
880 if (isset($normalizeData['billing_email'])) {
881 $cart->email = $normalizeData['billing_email'];
882 }
883
884 if (isset($normalizeData['billing_full_name'])) {
885 $fullName = $normalizeData['billing_full_name'];
886 $nameParts = explode(' ', $fullName);
887 $cart->first_name = array_shift($nameParts);
888 $cart->last_name = implode(' ', $nameParts);
889 }
890
891 $cart->fill($fillData);
892 $newTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
893 $taxAmountChanged = $oldTaxTotal !== $newTaxTotal;
894 $cart->clearFeeCache();
895
896 $cart = CartHelper::getCart();
897 $sanitizedUtmData = UtmHelper::getUtmDataOfRequest();
898 $cart->utm_data = $sanitizedUtmData;
899
900 $cart->save();
901
902 $fragments = [];
903
904 $cartRender = (new CheckoutRenderer($cart));
905 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
906 $enableModalCheckout = App::request()->get('modal_checkout');
907
908 if (!empty($hookChanges['shipping'])) {
909 $fragments[] = [
910 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]',
911 'content' => $cartRender->getFragment('shipping_methods'),
912 'type' => 'replace'
913 ];
914 }
915
916 if (array_filter($hookChanges)) {
917 $fragments[] = [
918 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
919 'content' => $cartRender->getFragment('cart_summary_fragment'),
920 'type' => 'replace'
921 ];
922
923 if ($taxAmountChanged || !empty($hookChanges['shipping'])) {
924 // also update the payment methods
925 if ($enableModalCheckout === 'yes') {
926 $fragments[] = [
927 'selector' => '[data-fluent-cart-checkout-payment-methods]',
928 'content' => $modalCheckoutRender->getFragment('payment_methods'),
929 'type' => 'replace'
930 ];
931 } else {
932 $fragments[] = [
933 'selector' => '[data-fluent-cart-checkout-payment-methods]',
934 'content' => $cartRender->getFragment('payment_methods'),
935 'type' => 'replace'
936 ];
937 }
938
939 $fragments[] = [
940 'selector' => '[data-fct-modal-checkout-summary-group]',
941 'content' => $modalCheckoutRender->getFragment('summary_group'),
942 'type' => 'replace'
943 ];
944
945 $fragments[] = [
946 'selector' => '[data-fct-modal-checkout-summary]',
947 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
948 'type' => 'replace'
949 ];
950 }
951 }
952
953 // Re-render cart summary on payment method change (fees may depend on payment method)
954 if (isset($normalizeData['_fct_pay_method'])) {
955 $hasSummaryFragment = false;
956 foreach ($fragments as $fragment) {
957 if (Arr::get($fragment, 'selector') === '[data-fluent-cart-checkout-page-cart-items-wrapper]') {
958 $hasSummaryFragment = true;
959 break;
960 }
961 }
962
963 if (!$hasSummaryFragment) {
964 $fragments[] = [
965 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
966 'content' => $cartRender->getFragment('cart_summary_fragment'),
967 'type' => 'replace'
968 ];
969
970 $fragments[] = [
971 'selector' => '[data-fct-modal-checkout-summary-group]',
972 'content' => $modalCheckoutRender->getFragment('summary_group'),
973 'type' => 'replace'
974 ];
975
976 $fragments[] = [
977 'selector' => '[data-fct-modal-checkout-summary]',
978 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
979 'type' => 'replace'
980 ];
981 }
982 }
983
984 if (($dataKey === 'billing_address_id' || $dataKey === 'shipping_address_id') && $dataValue !== '') {
985 // get current customer
986
987 $customer = CustomerResource::getCurrentCustomer();
988
989 $type = $dataKey === 'billing_address_id' ? 'billing' : 'shipping';
990 $requiredShipping = $cart->requireShipping();
991 $config = [
992 'type' => $type,
993 'product_type' => $requiredShipping ? 'physical' : 'digital',
994 'with_shipping' => $requiredShipping
995 ];
996 if ($type === 'billing') {
997 $config['with_shipping'] = Arr::get($allData, 'ship_to_different', 'no') !== 'yes';
998 $config['billing_address_id'] = $dataValue;
999 } else {
1000 $config['shipping_address_id'] = $dataValue;
1001 }
1002 $addresses = AddressHelper::getCustomerValidatedAddresses($config, $customer);
1003 $address = AddressHelper::getPrimaryAddress($addresses, $config, $customer, $type);
1004 $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements(
1005 $type,
1006 Arr::get($config, 'product_type'),
1007 Arr::get($config, 'with_shipping')
1008 );
1009 ob_start();
1010 (new AddressSelectRenderer($addresses, $address, $requirementsFields, $type))->renderAddressInfo();
1011 $addressRender = ob_get_clean();
1012
1013 $fragments[] = [
1014 'selector' => '#' . $type . '_address_wrapper [data-fluent-cart-checkout-page-form-address-info-wrapper]',
1015 'content' => $addressRender,
1016 'type' => 'replace'
1017 ];
1018 }
1019
1020 $fragments = apply_filters('fluent_cart/checkout/after_patch_checkout_data_fragments', $fragments, [
1021 'cart' => $cart,
1022 'changes' => $normalizeData
1023 ]);
1024
1025 return [
1026 'message' => __('Data saved successfully', 'fluent-cart'),
1027 'changes' => $normalizeData,
1028 'fragments' => $fragments,
1029 'cart' => $cart
1030 ];
1031 }
1032
1033 public function handleOrderBumpRequest()
1034 {
1035 $requestData = App::request()->all();
1036
1037 $cart = CartHelper::getCart();
1038 if (!$cart) {
1039 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
1040 }
1041
1042 $checkoutData = $cart->checkout_data;
1043 if (!empty($checkoutData['upgrade_data']) || !empty($checkoutData['is_locked'])) {
1044 return new \WP_Error('invalid_request', __('This cart is locked or already has an upgrade applied.', 'fluent-cart'));
1045 }
1046
1047 $upgradeFromVariationId = (int) Arr::get($requestData, 'upgrade_form', 0);
1048 $targetVariationId = (int) Arr::get($requestData, 'upgrade_to', 0);
1049 $bumpId = (int) Arr::get($requestData, 'bump_id', 0);
1050
1051 if ($bumpId) {
1052 $response = new \WP_Error('invalid_bump', __('Could not apply item at this time.', 'fluent-cart'));
1053 return apply_filters('fluent_cart/apply_order_bump', $response, [
1054 'bump_id' => $bumpId,
1055 'cart' => $cart,
1056 'request_data' => $requestData
1057 ]);
1058 }
1059
1060 if (!$upgradeFromVariationId || !$targetVariationId) {
1061 return new \WP_Error('invalid_request', __('Invalid upgrade request.', 'fluent-cart'));
1062 }
1063
1064 $productVariation = ProductVariation::query()->find($targetVariationId);
1065
1066 if (!$productVariation || !$productVariation->canPurchase()) {
1067 return new \WP_Error('invalid_variation', __('The selected product variation is not available for purchase.', 'fluent-cart'));
1068 }
1069
1070 $cart->removeItem($upgradeFromVariationId);
1071 $cart = $cart->addByVariation($productVariation, [
1072 'quantity' => 1,
1073 'append' => false
1074 ]);
1075
1076 if (is_wp_error($cart)) {
1077 return $cart;
1078 }
1079
1080 $isUpgraded = Arr::get($requestData, 'is_upgraded') === 'yes';
1081 if ($isUpgraded) {
1082 $checkoutData['order_bump'] = [
1083 'upgraded_from' => $upgradeFromVariationId,
1084 'upgraded_to' => $targetVariationId
1085 ];
1086 $existingActions = Arr::get($checkoutData, '__on_success_actions__', []);
1087 if (!is_array($existingActions)) {
1088 $existingActions = [];
1089 }
1090 $existingActions[] = 'fluent_cart/order_bump_succeed';
1091 $checkoutData['__on_success_actions__'] = $existingActions;
1092 } else {
1093 unset($checkoutData['order_bump']);
1094 $existingActions = Arr::get($checkoutData, '__on_success_actions__', []);
1095 if (!is_array($existingActions)) {
1096 $existingActions = [];
1097 }
1098
1099 if (!empty($existingActions) && in_array('fluent_cart/order_bump_succeed', $existingActions)) {
1100 $existingActions = array_diff($existingActions, ['fluent_cart/order_bump_succeed']);
1101 $existingActions = array_values($existingActions);
1102 }
1103
1104 if (is_array($existingActions)) {
1105 $existingActions = array_filter($existingActions, function ($action) {
1106 return $action !== 'fluent_cart/order_bump_succeed';
1107 });
1108 }
1109
1110 $checkoutData['__on_success_actions__'] = $existingActions;
1111 }
1112
1113 $cart->checkout_data = $checkoutData;
1114 $cart->save();
1115
1116 do_action('fluent_cart/checkout/cart_amount_updated', [
1117 'cart' => $cart
1118 ]);
1119
1120 return [
1121 'message' => $isUpgraded ? __('Item has been applied successfully', 'fluent-cart') : __('Item has been reverted successfully', 'fluent-cart')
1122 ];
1123 }
1124
1125
1126 public function normalizeCheckoutChangeData($changedData, $allData): array
1127 {
1128
1129 $sanitizedData = [];
1130 $errors = [];
1131 foreach ($changedData as $dataKey => $dataValue) {
1132 if (in_array($dataKey, ['billing_full_name', 'shipping_full_name'])) {
1133 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1134 } else if (in_array($dataKey, ['billing_email', 'shipping_email'])) {
1135 if (empty($dataValue)) {
1136 $sanitizedData[$dataKey] = '';
1137 } else {
1138 $sanitizedData[$dataKey] = sanitize_email($dataValue ?? '');
1139 }
1140 } else if (in_array($dataKey, ['billing_company_name', 'shipping_company_name', 'billing_legal_registration_id'])) {
1141 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1142 } else if ($dataKey === 'is_business') {
1143 $sanitizedData[$dataKey] = $dataValue === 'yes' ? 'yes' : 'no';
1144 } else if ($dataKey === 'fct_vat_declaration_note') {
1145 $sanitizedData[$dataKey] = mb_substr(sanitize_text_field($dataValue), 0, 255);
1146 } else if ($dataKey === 'billing_state' && !empty($dataValue)) {
1147 $billingCountry = Arr::get($allData, 'billing_country');
1148
1149 $states = LocalizationManager::getInstance()->statesOptions($billingCountry);
1150 $countryStates = array_values(array_column($states, 'value'));
1151 if (!empty($states) && !in_array($dataValue, $countryStates)) {
1152 $errors[$dataKey] = __('Invalid state code.', 'fluent-cart');
1153 $sanitizedData[$dataKey] = null;
1154 } else {
1155 $sanitizedData[$dataKey] = $dataValue;
1156 }
1157 } else if ($dataKey === 'shipping_state' && !empty($dataValue)) {
1158 $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes');
1159 $shippingCountry = $shipToDifferent ? Arr::get($allData, 'shipping_country') : Arr::get($allData, 'billing_country');
1160 $states = LocalizationManager::getInstance()->statesOptions($shippingCountry);
1161 if (!empty($states) && !in_array($dataValue, array_column($states, 'value'))) {
1162 $errors[$dataKey] = __('Invalid state code.', 'fluent-cart');
1163 $sanitizedData[$dataKey] = null;
1164 } else {
1165 $sanitizedData[$dataKey] = $dataValue;
1166 }
1167 } else if ($dataKey === '_fct_pay_method') {
1168 $value = sanitize_text_field($dataValue);
1169 $methods = PaymentMethods::getActiveMeta();
1170 $methods = array_column($methods, 'route');
1171 if (!in_array($value, $methods)) {
1172 $value = null;
1173 $errors[$dataKey] = __('Invalid payment method.', 'fluent-cart');
1174 }
1175 $sanitizedData[$dataKey] = $value;
1176 } else if ($dataKey === 'shipping_method_id') {
1177 $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes');
1178 $countryKey = $shipToDifferent ? 'shipping_country' : 'billing_country';
1179 $stateKey = $shipToDifferent ? 'shipping_state' : 'billing_state';
1180 $shippingCountry = Arr::has($changedData, $countryKey) ? Arr::get($changedData, $countryKey) : Arr::get($allData, $countryKey);
1181 $shippingState = Arr::has($changedData, $stateKey) ? Arr::get($changedData, $stateKey) : Arr::get($allData, $stateKey);
1182 $availableShippingMethods = AddressHelper::getShippingMethods($shippingCountry, $shippingState);
1183
1184 $found = false;
1185 foreach ($availableShippingMethods as $method) {
1186 if ($method->id == $dataValue) {
1187 $found = true;
1188 break;
1189 }
1190 }
1191
1192 if (!$found) {
1193 $errors[$dataKey] = __('Invalid shipping method.', 'fluent-cart');
1194 $sanitizedData[$dataKey] = null;
1195 } else {
1196 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1197 }
1198 } else {
1199 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1200 }
1201 }
1202
1203 return $sanitizedData;
1204 }
1205
1206 private function pushAddressData($data, $type = 'billing')
1207 {
1208 return AddressHelper::maybePushAddressDataForCheckout($data, $type);
1209 }
1210
1211 public function getProductModalView()
1212 {
1213 $productId = App::request()->getSafe('product_id', 'intval');
1214 $product = Product::query()->find($productId);
1215
1216 if (!$product) {
1217 return $this->sendError([
1218 'message' => __('Product not found', 'fluent-cart')
1219 ]);
1220 }
1221 ob_start();
1222 (new ProductModalRenderer($product))->render();
1223 $view = ob_get_clean();
1224 return [
1225 'view' => $view
1226 ];
1227 }
1228
1229 }
1230