PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.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 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.6.0, at app/Hooks/Cart/WebCheckoutHandler.php

1,227 lines 46.1 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 ];
794
795 $addressFieldKeys = [
796 'full_name',
797 'country',
798 'address_1',
799 'address_2',
800 'state',
801 'city',
802 'postcode',
803 'company_name',
804 'phone'
805 ];
806
807 foreach ($addressFieldKeys as $addressFieldKey) {
808 $validKeys['billing_' . $addressFieldKey] = 'form_data.billing_' . $addressFieldKey;
809 $validKeys['shipping_' . $addressFieldKey] = 'form_data.shipping_' . $addressFieldKey;
810 }
811
812 $prevFlatData = [];
813 $prevCheckoutData = $cart->checkout_data;
814 foreach ($validKeys as $dataName => $dataPath) {
815 $prevFlatData[$dataName] = Arr::get($prevCheckoutData, $dataPath, null);
816 }
817 // $prevFlatData = array_filter($prevFlatData);
818 $prevFlatData = array_filter($prevFlatData, function ($value) {
819 return $value !== null;
820 });
821 $validData = Arr::only($allData, array_keys($validKeys));
822
823
824 $normalizeData = [];
825
826 foreach ($validData as $key => $value) {
827 $prevValue = Arr::get($prevFlatData, $key, null);
828 if ($prevValue != $value) {
829 $normalizeData[$key] = $value;
830 }
831 }
832
833 // Force-check the explicit data_key field — bulk POST may have already stored it as a side-effect.
834 if ($dataKey && isset($validKeys[$dataKey]) && !isset($normalizeData[$dataKey])) {
835 $storedValue = (string) Arr::get($prevCheckoutData, $validKeys[$dataKey], '');
836 if ($storedValue !== $dataValue) {
837 $normalizeData[$dataKey] = $dataValue;
838 }
839 }
840
841 if (!$normalizeData) {
842 return [
843 'message' => __('No changes detected', 'fluent-cart')
844 ];
845 }
846
847 $normalizeData = $this->normalizeCheckoutChangeData($normalizeData, $allData);
848
849 // Validate these data and filter out invalid data points and also push the
850 // address data into this array.
851
852
853 $checkoutData = $cart->checkout_data;
854 foreach ($normalizeData as $normalizeKey => $normalizeValue) {
855 Arr::set($checkoutData, $validKeys[$normalizeKey], $normalizeValue);
856 }
857
858 $oldTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
859 $fillData = [
860 'checkout_data' => $checkoutData,
861 'cart_data' => $cart->cart_data,
862 'hook_changes' => [
863 'shipping' => false,
864 'tax' => false
865 ]
866 ];
867
868
869 $fillData = apply_filters('fluent_cart/checkout/before_patch_checkout_data', $fillData, [
870 'cart' => $cart,
871 'prev_data' => $prevFlatData,
872 'changes' => $normalizeData,
873 'all_data' => $allData
874 ]);
875
876 $hookChanges = Arr::get($fillData, 'hook_changes', []);
877 unset($fillData['hook_changes']);
878
879 if (isset($normalizeData['billing_email'])) {
880 $cart->email = $normalizeData['billing_email'];
881 }
882
883 if (isset($normalizeData['billing_full_name'])) {
884 $fullName = $normalizeData['billing_full_name'];
885 $nameParts = explode(' ', $fullName);
886 $cart->first_name = array_shift($nameParts);
887 $cart->last_name = implode(' ', $nameParts);
888 }
889
890 $cart->fill($fillData);
891 $newTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
892 $taxAmountChanged = $oldTaxTotal !== $newTaxTotal;
893 $cart->clearFeeCache();
894
895 $cart = CartHelper::getCart();
896 $sanitizedUtmData = UtmHelper::getUtmDataOfRequest();
897 $cart->utm_data = $sanitizedUtmData;
898
899 $cart->save();
900
901 $fragments = [];
902
903 $cartRender = (new CheckoutRenderer($cart));
904 $modalCheckoutRender = (new ModalCheckoutRenderer($cart));
905 $enableModalCheckout = App::request()->get('modal_checkout');
906
907 if (!empty($hookChanges['shipping'])) {
908 $fragments[] = [
909 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]',
910 'content' => $cartRender->getFragment('shipping_methods'),
911 'type' => 'replace'
912 ];
913 }
914
915 if (array_filter($hookChanges)) {
916 $fragments[] = [
917 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
918 'content' => $cartRender->getFragment('cart_summary_fragment'),
919 'type' => 'replace'
920 ];
921
922 if ($taxAmountChanged || !empty($hookChanges['shipping'])) {
923 // also update the payment methods
924 if ($enableModalCheckout === 'yes') {
925 $fragments[] = [
926 'selector' => '[data-fluent-cart-checkout-payment-methods]',
927 'content' => $modalCheckoutRender->getFragment('payment_methods'),
928 'type' => 'replace'
929 ];
930 } else {
931 $fragments[] = [
932 'selector' => '[data-fluent-cart-checkout-payment-methods]',
933 'content' => $cartRender->getFragment('payment_methods'),
934 'type' => 'replace'
935 ];
936 }
937
938 $fragments[] = [
939 'selector' => '[data-fct-modal-checkout-summary-group]',
940 'content' => $modalCheckoutRender->getFragment('summary_group'),
941 'type' => 'replace'
942 ];
943
944 $fragments[] = [
945 'selector' => '[data-fct-modal-checkout-summary]',
946 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
947 'type' => 'replace'
948 ];
949 }
950 }
951
952 // Re-render cart summary on payment method change (fees may depend on payment method)
953 if (isset($normalizeData['_fct_pay_method'])) {
954 $hasSummaryFragment = false;
955 foreach ($fragments as $fragment) {
956 if (Arr::get($fragment, 'selector') === '[data-fluent-cart-checkout-page-cart-items-wrapper]') {
957 $hasSummaryFragment = true;
958 break;
959 }
960 }
961
962 if (!$hasSummaryFragment) {
963 $fragments[] = [
964 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
965 'content' => $cartRender->getFragment('cart_summary_fragment'),
966 'type' => 'replace'
967 ];
968
969 $fragments[] = [
970 'selector' => '[data-fct-modal-checkout-summary-group]',
971 'content' => $modalCheckoutRender->getFragment('summary_group'),
972 'type' => 'replace'
973 ];
974
975 $fragments[] = [
976 'selector' => '[data-fct-modal-checkout-summary]',
977 'content' => $modalCheckoutRender->getFragment('checkout_summary'),
978 'type' => 'replace'
979 ];
980 }
981 }
982
983 if (($dataKey === 'billing_address_id' || $dataKey === 'shipping_address_id') && $dataValue !== '') {
984 // get current customer
985
986 $customer = CustomerResource::getCurrentCustomer();
987
988 $type = $dataKey === 'billing_address_id' ? 'billing' : 'shipping';
989 $requiredShipping = $cart->requireShipping();
990 $config = [
991 'type' => $type,
992 'product_type' => $requiredShipping ? 'physical' : 'digital',
993 'with_shipping' => $requiredShipping
994 ];
995 if ($type === 'billing') {
996 $config['with_shipping'] = Arr::get($allData, 'ship_to_different', 'no') !== 'yes';
997 $config['billing_address_id'] = $dataValue;
998 } else {
999 $config['shipping_address_id'] = $dataValue;
1000 }
1001 $addresses = AddressHelper::getCustomerValidatedAddresses($config, $customer);
1002 $address = AddressHelper::getPrimaryAddress($addresses, $config, $customer, $type);
1003 $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements(
1004 $type,
1005 Arr::get($config, 'product_type'),
1006 Arr::get($config, 'with_shipping')
1007 );
1008 ob_start();
1009 (new AddressSelectRenderer($addresses, $address, $requirementsFields, $type))->renderAddressInfo();
1010 $addressRender = ob_get_clean();
1011
1012 $fragments[] = [
1013 'selector' => '#' . $type . '_address_wrapper [data-fluent-cart-checkout-page-form-address-info-wrapper]',
1014 'content' => $addressRender,
1015 'type' => 'replace'
1016 ];
1017 }
1018
1019 $fragments = apply_filters('fluent_cart/checkout/after_patch_checkout_data_fragments', $fragments, [
1020 'cart' => $cart,
1021 'changes' => $normalizeData
1022 ]);
1023
1024 return [
1025 'message' => __('Data saved successfully', 'fluent-cart'),
1026 'changes' => $normalizeData,
1027 'fragments' => $fragments,
1028 'cart' => $cart
1029 ];
1030 }
1031
1032 public function handleOrderBumpRequest()
1033 {
1034 $requestData = App::request()->all();
1035
1036 $cart = CartHelper::getCart();
1037 if (!$cart) {
1038 return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart'));
1039 }
1040
1041 $checkoutData = $cart->checkout_data;
1042 if (!empty($checkoutData['upgrade_data']) || !empty($checkoutData['is_locked'])) {
1043 return new \WP_Error('invalid_request', __('This cart is locked or already has an upgrade applied.', 'fluent-cart'));
1044 }
1045
1046 $upgradeFromVariationId = (int) Arr::get($requestData, 'upgrade_form', 0);
1047 $targetVariationId = (int) Arr::get($requestData, 'upgrade_to', 0);
1048 $bumpId = (int) Arr::get($requestData, 'bump_id', 0);
1049
1050 if ($bumpId) {
1051 $response = new \WP_Error('invalid_bump', __('Could not apply item at this time.', 'fluent-cart'));
1052 return apply_filters('fluent_cart/apply_order_bump', $response, [
1053 'bump_id' => $bumpId,
1054 'cart' => $cart,
1055 'request_data' => $requestData
1056 ]);
1057 }
1058
1059 if (!$upgradeFromVariationId || !$targetVariationId) {
1060 return new \WP_Error('invalid_request', __('Invalid upgrade request.', 'fluent-cart'));
1061 }
1062
1063 $productVariation = ProductVariation::query()->find($targetVariationId);
1064
1065 if (!$productVariation || !$productVariation->canPurchase()) {
1066 return new \WP_Error('invalid_variation', __('The selected product variation is not available for purchase.', 'fluent-cart'));
1067 }
1068
1069 $cart->removeItem($upgradeFromVariationId);
1070 $cart = $cart->addByVariation($productVariation, [
1071 'quantity' => 1,
1072 'append' => false
1073 ]);
1074
1075 if (is_wp_error($cart)) {
1076 return $cart;
1077 }
1078
1079 $isUpgraded = Arr::get($requestData, 'is_upgraded') === 'yes';
1080 if ($isUpgraded) {
1081 $checkoutData['order_bump'] = [
1082 'upgraded_from' => $upgradeFromVariationId,
1083 'upgraded_to' => $targetVariationId
1084 ];
1085 $existingActions = Arr::get($checkoutData, '__on_success_actions__', []);
1086 if (!is_array($existingActions)) {
1087 $existingActions = [];
1088 }
1089 $existingActions[] = 'fluent_cart/order_bump_succeed';
1090 $checkoutData['__on_success_actions__'] = $existingActions;
1091 } else {
1092 unset($checkoutData['order_bump']);
1093 $existingActions = Arr::get($checkoutData, '__on_success_actions__', []);
1094 if (!is_array($existingActions)) {
1095 $existingActions = [];
1096 }
1097
1098 if (!empty($existingActions) && in_array('fluent_cart/order_bump_succeed', $existingActions)) {
1099 $existingActions = array_diff($existingActions, ['fluent_cart/order_bump_succeed']);
1100 $existingActions = array_values($existingActions);
1101 }
1102
1103 if (is_array($existingActions)) {
1104 $existingActions = array_filter($existingActions, function ($action) {
1105 return $action !== 'fluent_cart/order_bump_succeed';
1106 });
1107 }
1108
1109 $checkoutData['__on_success_actions__'] = $existingActions;
1110 }
1111
1112 $cart->checkout_data = $checkoutData;
1113 $cart->save();
1114
1115 do_action('fluent_cart/checkout/cart_amount_updated', [
1116 'cart' => $cart
1117 ]);
1118
1119 return [
1120 'message' => $isUpgraded ? __('Item has been applied successfully', 'fluent-cart') : __('Item has been reverted successfully', 'fluent-cart')
1121 ];
1122 }
1123
1124
1125 public function normalizeCheckoutChangeData($changedData, $allData): array
1126 {
1127
1128 $sanitizedData = [];
1129 $errors = [];
1130 foreach ($changedData as $dataKey => $dataValue) {
1131 if (in_array($dataKey, ['billing_full_name', 'shipping_full_name'])) {
1132 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1133 } else if (in_array($dataKey, ['billing_email', 'shipping_email'])) {
1134 if (empty($dataValue)) {
1135 $sanitizedData[$dataKey] = '';
1136 } else {
1137 $sanitizedData[$dataKey] = sanitize_email($dataValue ?? '');
1138 }
1139 } else if (in_array($dataKey, ['billing_company_name', 'shipping_company_name', 'billing_legal_registration_id'])) {
1140 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1141 } else if ($dataKey === 'is_business') {
1142 $sanitizedData[$dataKey] = $dataValue === 'yes' ? 'yes' : 'no';
1143 } else if ($dataKey === 'billing_state' && !empty($dataValue)) {
1144 $billingCountry = Arr::get($allData, 'billing_country');
1145
1146 $states = LocalizationManager::getInstance()->statesOptions($billingCountry);
1147 $countryStates = array_values(array_column($states, 'value'));
1148 if (!empty($states) && !in_array($dataValue, $countryStates)) {
1149 $errors[$dataKey] = __('Invalid state code.', 'fluent-cart');
1150 $sanitizedData[$dataKey] = null;
1151 } else {
1152 $sanitizedData[$dataKey] = $dataValue;
1153 }
1154 } else if ($dataKey === 'shipping_state' && !empty($dataValue)) {
1155 $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes');
1156 $shippingCountry = $shipToDifferent ? Arr::get($allData, 'shipping_country') : Arr::get($allData, 'billing_country');
1157 $states = LocalizationManager::getInstance()->statesOptions($shippingCountry);
1158 if (!empty($states) && !in_array($dataValue, array_column($states, 'value'))) {
1159 $errors[$dataKey] = __('Invalid state code.', 'fluent-cart');
1160 $sanitizedData[$dataKey] = null;
1161 } else {
1162 $sanitizedData[$dataKey] = $dataValue;
1163 }
1164 } else if ($dataKey === '_fct_pay_method') {
1165 $value = sanitize_text_field($dataValue);
1166 $methods = PaymentMethods::getActiveMeta();
1167 $methods = array_column($methods, 'route');
1168 if (!in_array($value, $methods)) {
1169 $value = null;
1170 $errors[$dataKey] = __('Invalid payment method.', 'fluent-cart');
1171 }
1172 $sanitizedData[$dataKey] = $value;
1173 } else if ($dataKey === 'shipping_method_id') {
1174 $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes');
1175 $countryKey = $shipToDifferent ? 'shipping_country' : 'billing_country';
1176 $stateKey = $shipToDifferent ? 'shipping_state' : 'billing_state';
1177 $shippingCountry = Arr::has($changedData, $countryKey) ? Arr::get($changedData, $countryKey) : Arr::get($allData, $countryKey);
1178 $shippingState = Arr::has($changedData, $stateKey) ? Arr::get($changedData, $stateKey) : Arr::get($allData, $stateKey);
1179 $availableShippingMethods = AddressHelper::getShippingMethods($shippingCountry, $shippingState);
1180
1181 $found = false;
1182 foreach ($availableShippingMethods as $method) {
1183 if ($method->id == $dataValue) {
1184 $found = true;
1185 break;
1186 }
1187 }
1188
1189 if (!$found) {
1190 $errors[$dataKey] = __('Invalid shipping method.', 'fluent-cart');
1191 $sanitizedData[$dataKey] = null;
1192 } else {
1193 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1194 }
1195 } else {
1196 $sanitizedData[$dataKey] = sanitize_text_field($dataValue);
1197 }
1198 }
1199
1200 return $sanitizedData;
1201 }
1202
1203 private function pushAddressData($data, $type = 'billing')
1204 {
1205 return AddressHelper::maybePushAddressDataForCheckout($data, $type);
1206 }
1207
1208 public function getProductModalView()
1209 {
1210 $productId = App::request()->getSafe('product_id', 'intval');
1211 $product = Product::query()->find($productId);
1212
1213 if (!$product) {
1214 return $this->sendError([
1215 'message' => __('Product not found', 'fluent-cart')
1216 ]);
1217 }
1218 ob_start();
1219 (new ProductModalRenderer($product))->render();
1220 $view = ob_get_clean();
1221 return [
1222 'view' => $view
1223 ];
1224 }
1225
1226 }
1227