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', '')); $order->updateMeta('_order_bump', [ 'upgraded_from' => Arr::get($cart->checkout_data, 'order_bump.upgraded_from', ''), 'upgraded_to' => Arr::get($cart->checkout_data, 'order_bump.upgraded_to', '') ]); }); } public function globalCheckoutRouteHandler() { nocache_headers(); $nonce = isset($_SERVER['HTTP_X_WP_NONCE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'])) : ''; if (!wp_verify_nonce($nonce, 'fluentcart') && !wp_verify_nonce($nonce, 'wp_rest')) { wp_send_json(['message' => __('Invalid nonce', 'fluent-cart')], 403); return; } $startedAt = microtime(true); $action = App::request()->get('fc_checkout_action'); $result = []; switch ($action) { case 'apply_coupon': $result = $this->handleApplyCouponAjax(); break; case 'remove_coupon': $result = $this->handleRemoveCouponAjax(); break; case 'get_checkout_summary_view': case 'reapply_coupon': $result = $this->handleGetCheckoutSummaryViewAjax(); break; case 'get_order_info': $result = $this->handleGetOrderInfoAjax(); break; case 'save_checkout_data': $result = $this->patchCheckoutData(); break; case 'get_country_info': $result = $this->handleGetCountryInfoAjax(); break; case 'update_address_select': $result = $this->handleUpdateAddressSelectAjax(); break; case 'get_shipping_methods_list_view': $result = $this->handleGetShippingMethodsListViewAjax(); break; case 'fluent_cart_cart_update': $result = $this->handleCartUpdateAjax(); break; case 'fluent_cart_cart_status': $result = $this->handleCartStatusAjax(); break; case 'apply_order_bump': $result = $this->handleOrderBumpRequest(); break; case 'get_product_modal_view': $result = $this->getProductModalView(); break; } if (is_wp_error($result)) { wp_send_json([ 'message' => Arr::get($result, 'message') ?? $result->get_error_message() ], 422); } if (is_array($result)) { $result['_bench'] = microtime(true) - $startedAt; } wp_send_json($result, 200); } public function handlePlaceOrderAjax() { nocache_headers(); $data = App::request()->all(); CheckoutApi::placeOrder($data, true); } public function getShippingChargeData($cart): array { $shippingMethodId = App::request()->getSafe('shipping_method_id', 'sanitize_text_field'); if (!$shippingMethodId) { $shippingMethodId = Arr::get($cart->checkout_data, 'shipping_data.shipping_method_id'); } $shippingMethod = ShippingMethod::query()->find($shippingMethodId); $charge = 0; if (!empty($shippingMethod)) { $shippingCountry = Arr::get($cart->checkout_data, 'form_data.shipping_country'); $shippingState = Arr::get($cart->checkout_data, 'form_data.shipping_state'); $lists = AddressHelper::getAvailableShippingMethodLists(['country_code' => $shippingCountry, 'state' => $shippingState]); if (isset($lists['available_shipping_methods'])) { $availableShippingMethods = Arr::get($lists, 'available_shipping_methods', []); $shippingMethodIds = Arr::pluck($availableShippingMethods, 'id'); if (in_array($shippingMethodId, $shippingMethodIds)) { $shippingMethod = ShippingMethod::query()->find($shippingMethodId); if (!empty($shippingMethod)) { $charge = CartHelper::calculateShippingMethodCharge($shippingMethod); } } else { CartHelper::resetShippingCharge(); } } else { CartHelper::resetShippingCharge(); } } return [ 'charge' => $charge, 'formatted_charge' => Helper::toDecimal($charge), 'shippingMethodId' => $shippingMethodId ]; } public function handleGetOrderInfoAjax() { $method = App::request()->getSafe('method', 'sanitize_text_field'); $paymentManager = GatewayManager::getInstance()->get($method); return $paymentManager->getOrderInfo(App::request()->all()); } public function handleApplyCouponAjax() { $couponCode = App::request()->getSafe('coupon_code', 'sanitize_text_field'); $cart = CartHelper::getCart(null, false); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $result = $cart->applyCoupon($couponCode); if (is_wp_error($result)) { return $result; } $cartCouponCodes = is_array($cart->coupons) ? $cart->coupons : []; $couponWasApplied = false; foreach ($cartCouponCodes as $existingCode) { if (strcasecmp((string) $existingCode, (string) $couponCode) === 0) { $couponWasApplied = true; break; } } if (!$couponWasApplied) { $perCouponResults = is_array($result) ? Arr::get($result, 'coupon_results', []) : []; $errorMessage = ''; foreach ($perCouponResults as $resultCode => $couponResult) { if (strcasecmp((string) $resultCode, (string) $couponCode) === 0) { $errorMessage = Arr::get($couponResult, 'error', ''); if ($errorMessage !== '') { break; } } } if ($errorMessage === '') { $errorMessage = __('No matching coupon found for this code.', 'fluent-cart'); } return new \WP_Error('coupon_not_applied', $errorMessage); } ob_start(); (new CartSummaryRender($cart))->render($withWrapper = false); $summary = ob_get_clean(); $cartTotal = $cart->getEstimatedTotal(); $modalCheckoutRender = (new ModalCheckoutRenderer($cart)); $enableModalCheckout = App::request()->get('modal_checkout'); $fragments = []; $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $summary, 'type' => 'replace' ]; if ($enableModalCheckout === 'yes') { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $modalCheckoutRender->getFragment('payment_methods'), 'type' => 'replace' ]; } else { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => (new CheckoutRenderer($cart))->getFragment('payment_methods'), 'type' => 'replace' ]; } $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary-group]', 'content' => $modalCheckoutRender->getFragment('summary_group'), 'type' => 'replace' ]; $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary]', 'content' => $modalCheckoutRender->getFragment('checkout_summary'), 'type' => 'replace' ]; return [ 'fragments' => $fragments, 'cart' => $cart, 'total' => $cartTotal, 'formatted_total' => Helper::toDecimal($cartTotal), 'applied_coupons' => $cart->coupons, 'has_subscriptions' => $cart->hasSubscription() ]; } public function handleRemoveCouponAjax() { $couponCode = App::request()->getSafe('coupon_code', 'sanitize_text_field'); $cart = CartHelper::getCart(null, false); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $cart = $cart->removeCoupon($couponCode); if (is_wp_error($cart)) { return $cart; } ob_start(); (new CartSummaryRender($cart))->render($withWrapper = false); $summary = ob_get_clean(); $cartTotal = $cart->getEstimatedTotal(); $modalCheckoutRender = (new ModalCheckoutRenderer($cart)); $enableModalCheckout = App::request()->get('modal_checkout'); $fragments = []; $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $summary, 'type' => 'replace' ]; if ($enableModalCheckout === 'yes') { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $modalCheckoutRender->getFragment('payment_methods'), 'type' => 'replace' ]; } else { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => (new CheckoutRenderer($cart))->getFragment('payment_methods'), 'type' => 'replace' ]; } $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary-group]', 'content' => $modalCheckoutRender->getFragment('summary_group'), 'type' => 'replace' ]; $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary]', 'content' => $modalCheckoutRender->getFragment('checkout_summary'), 'type' => 'replace' ]; return [ 'fragments' => $fragments, 'total' => $cartTotal, 'formatted_total' => Helper::toDecimal($cartTotal), 'applied_coupons' => $cart->coupons, 'has_subscriptions' => $cart->hasSubscription() ]; } public function handleGetCheckoutSummaryViewAjax() { $cart = CartHelper::getCart(null, false); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $cart = $cart->reValidateCoupons(); $checkOutHelper = CartCheckoutHelper::make(); $checkOutHelper->setCart($cart); $shippingChargeData = $this->getShippingChargeData($cart); $shippingCharge = Arr::get($shippingChargeData, 'charge'); $shippingMethodId = Arr::get($shippingChargeData, 'shippingMethodId'); $formattedShippingCharge = Arr::get($shippingChargeData, 'formatted_charge'); $subtotal = $checkOutHelper->getItemsAmountSubtotal(false); $formattedSubtotal = $checkOutHelper->getItemsAmountSubtotal(true); $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge; // Include fee total $feeTotal = $cart->getFeeTotal(); if ($feeTotal > 0) { $totalPrice += $feeTotal; } if (!empty($cart->checkout_data['tax_data'])) { $taxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $totalPrice += $taxTotal; } $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0); if (!empty($shippingCharge)) { $cart->checkout_data = array_merge($cart->checkout_data, [ 'shipping_data' => [ 'shipping_method_id' => $shippingMethodId, 'shipping_charge' => $shippingCharge ] ]); } else { $cart->checkout_data = array_merge($cart->checkout_data, [ 'shipping_data' => [ 'shipping_method_id' => null, 'shipping_charge' => 0 ] ]); } $cart->save(); // shipping charge changed if ($shippingCharge !== $oldShippingCharge) { do_action('fluent_cart/checkout/shipping_data_changed', [ 'cart' => $cart ]); } $totalPrice = apply_filters('fluent_cart/cart/estimated_total', $totalPrice, [ 'cart' => $cart ]); // Prorate credit and upgrade discount (plan upgrade) are post-tax adjustments on // the payable total. $totalPrice = max(0, $totalPrice - (int) Arr::get($cart->checkout_data, 'prorate_credit.amount', 0) - (int) Arr::get($cart->checkout_data, 'upgrade_discount.amount', 0)); $fragments = []; $cartRender = (new CheckoutRenderer($cart)); $modalCheckoutRender = (new ModalCheckoutRenderer($cart)); $enableModalCheckout = App::request()->getSafe('modal_checkout', 'sanitize_text_field'); ob_start(); (new CartSummaryRender($cart))->render(false); $cartSummaryInner = ob_get_clean(); $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $cartSummaryInner, 'type' => 'replace' ]; if ($enableModalCheckout === 'yes') { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $modalCheckoutRender->getFragment('payment_methods'), 'type' => 'replace' ]; } else { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $cartRender->getFragment('payment_methods'), 'type' => 'replace' ]; } return [ 'fragments' => $fragments, 'total' => $totalPrice, 'applied_coupons' => $cart->coupons, 'shipping_charge' => $shippingCharge, 'formatted_shipping_charge' => $formattedShippingCharge, 'has_subscriptions' => $cart->hasSubscription(), 'shipping_method_id' => $shippingMethodId ]; } public function handleUpdateAddressSelectAjax() { $customerAddressId = App::request()->get('customer_address_id'); $address = CustomerAddressResource::find($customerAddressId, ['with' => App::request()->get('with', [])]); if (!$address) { return [ 'message' => __('Address not found', 'fluent-cart') ]; } //update address into cart $addressId = Arr::get($address, 'address.id'); $country = Arr::get($address, 'address.country'); $state = Arr::get($address, 'address.state'); $type = Arr::get($address, 'address.type', 'billing'); $cart = CartHelper::getCart(App::request()->get('fct_cart_hash')); $oldTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $oldShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0); $previousFormData = Arr::get($cart->checkout_data, 'form_data', []); $checkoutData = Arr::wrap($cart->checkout_data); Arr::set($checkoutData, 'form_data.' . $type . '_address_id', $addressId); Arr::set($checkoutData, 'form_data.' . $type . '_country', $country); Arr::set($checkoutData, 'form_data.' . $type . '_state', $state); if ($type === 'billing' && Arr::get($checkoutData, 'form_data.ship_to_different', 'no') === 'no') { Arr::set($checkoutData, 'form_data.shipping_address_id', $addressId); Arr::set($checkoutData, 'form_data.shipping_country', $country); Arr::set($checkoutData, 'form_data.shipping_state', $state); } $checkoutData = (new TaxModule())->maybeInvalidateVatValidationForCountryChange($checkoutData, [ 'ship_to_different' => Arr::get($previousFormData, 'ship_to_different', 'no'), 'billing_country' => Arr::get($previousFormData, 'billing_country', ''), 'shipping_country' => Arr::get($previousFormData, 'shipping_country', ''), ]); $cart->checkout_data = $checkoutData; $cart->save(); $customerId = Arr::get($address, 'address.customer_id'); $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer(); if (empty($customer) || $customer->id != $customerId) { return [ 'message' => __('You are not authorized to view this address', 'fluent-cart') ]; } $formattedAddress = Arr::get($address, 'address.formatted_address'); // Use output buffering to generate HTML ob_start(); // Extract the address parts $addressParts = [ trim(Arr::get($formattedAddress, 'address_1') ?? ''), trim(Arr::get($formattedAddress, 'address_2') ?? ''), trim(Arr::get($formattedAddress, 'city') ?? ''), trim(Arr::get($formattedAddress, 'state') ?? ''), trim(Arr::get($formattedAddress, 'country') ?? ''), ]; // Filter out empty or null parts $addressParts = array_filter($addressParts, function ($part) { return $part !== ''; }); // Join parts with comma and space do_action('fluent_cart/checkout/form_data_changed', [ 'cart' => $cart ]); ob_start(); (new CartSummaryRender($cart))->render(false); $cartSummaryInner = ob_get_clean(); $newTaxTotal = Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $newShippingCharge = Arr::get($cart->checkout_data, 'shipping_data.shipping_charge', 0); $checkoutData = [ 'message' => __('Address Attached', 'fluent-cart'), 'fragments' => [ [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $cartSummaryInner, 'type' => 'replace' ] ], 'tax_total_changes' => $oldTaxTotal != $newTaxTotal, 'shipping_charge_changes' => $oldShippingCharge != $newShippingCharge ]; return apply_filters('fluent_cart/checkout/checkout_data_changed', $checkoutData, ['cart' => $cart]); } public function handleGetCountryInfoAjax() { $timezone = App::request()->getSafe('timezone', 'sanitize_text_field'); $code = App::request()->getSafe('country_code', 'sanitize_text_field'); $countryInfo = LocalizationManager::getCountryInfoFromRequest($timezone, $code); return [ 'country_info' => $countryInfo ]; } public function getShippingMethodsListView(array $data) { $availableShippingMethods = AddressHelper::getAvailableShippingMethodLists($data); $shippingMethods = Arr::get($availableShippingMethods, 'available_shipping_methods'); $countryCode = Arr::get($availableShippingMethods, 'country_code'); $status = Arr::get($availableShippingMethods, 'status'); if ($status === false) { return false; } $cart = CartHelper::getCart(); $cartRender = (new CheckoutRenderer($cart)); ob_start(); $cartRender->getFragment('shipping_methods'); $content = ob_get_clean(); return [ 'view' => $content, 'country_code' => $countryCode, 'shipping_methods' => $shippingMethods ]; } public function handleGetShippingMethodsListViewAjax() { $data = [ 'country_code' => App::request()->get('country_code'), 'state' => App::request()->get('state'), 'timezone' => App::request()->get('timezone') ]; $cart = CartHelper::getCart(); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $availableShippingMethods = AddressHelper::getShippingMethods($data['country_code'], $data['state'], $data['timezone']); ob_start(); $selectedId = Arr::get($cart->checkout_data, 'shipping_data.shipping_method_id', ''); $selectedId = CartHelper::resolveAutoSelectShippingMethod($cart, $availableShippingMethods ?: [], $selectedId); if (!$availableShippingMethods || is_wp_error($availableShippingMethods)) { (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render(); } else { foreach ($availableShippingMethods as $method) { $method->charge_amount = CartHelper::calculateShippingMethodCharge($method, $cart->cart_data); } (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render(); } $shippingMethodsView = ob_get_clean(); return [ 'status' => true, 'fragments' => [ [ 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]', 'content' => $shippingMethodsView, 'type' => 'replace' ] ], 'country_code' => $data['country_code'], 'shipping_method_id' => $selectedId ?: null, ]; } public function handleCartStatusAjax() { return CartResource::getStatus(); } public function handleCartUpdateAjax() { $requestData = App::request()->all(); $data = [ 'item_id' => (int)Arr::get($requestData, 'item_id'), 'quantity' => (int)Arr::get($requestData, 'quantity', 0), 'by_input' => Arr::get($requestData, 'by_input', false), 'is_custom' => Arr::get($requestData, 'is_custom', false) ]; $cart = CartResource::update($data, '', $requestData); if (is_wp_error($cart)) { return $cart; } do_action('fluent_cart/checkout/cart_amount_updated', [ 'cart' => $cart ]); $itemCount = 0; if ($cart) { $itemCount = count($cart->cart_data ?? []); } $cartItems = Arr::get(CartResource::getStatus(), 'cart_data', []); $defaultOpen = Arr::get($requestData, 'open_cart', false); $isAdminBarEnabled = Arr::get($requestData, 'is_admin_bar_enabled', false); $fragments = []; if (!empty($cartItems)) { ob_start(); (new CartDrawerRenderer($cartItems, [ 'item_count' => $itemCount, 'open_cart' => $defaultOpen, 'is_admin_bar_enabled' => $isAdminBarEnabled ]))->render(); $cartDrawerView = ob_get_clean(); ob_start(); (new CartRenderer($cartItems))->renderItems($cartItems); $cartDrawerItemsView = ob_get_clean(); ob_start(); (new CartRenderer($cartItems))->renderTotal(); $cartDrawerItemsTotalView = ob_get_clean(); ob_start(); (new CartDrawerRenderer($cartItems, [ 'item_count' => $itemCount, 'open_cart' => $defaultOpen, 'is_admin_bar_enabled' => $isAdminBarEnabled ]))->renderItemCount(); $cartItemCount = ob_get_clean(); $fragments = [ [ 'selector' => '[data-fluent-cart-cart-drawer-container]', 'content' => $cartDrawerView, 'type' => 'replace' ], [ 'selector' => '[data-fluent-cart-cart-content-wrapper]', 'content' => $cartDrawerItemsView, 'type' => 'replace' ], [ 'selector' => '[data-fluent-cart-cart-total-wrapper]', 'content' => $cartDrawerItemsTotalView, 'type' => 'replace' ], [ 'selector' => '.fluent-cart-cart-badge-count', 'content' => $itemCount > 0 ? $cartItemCount : '', 'type' => 'replace' ] ]; } if (empty($cartItems)) { ob_start(); (new CartRenderer($cartItems))->renderEmpty(); $cartDrawerItemsView = ob_get_clean(); $fragments[] = [ 'selector' => '[data-fluent-cart-cart-content-wrapper]', 'content' => $cartDrawerItemsView, 'type' => 'replace' ]; } return [ 'message' => __('Cart updated successfully', 'fluent-cart'), 'data' => apply_filters('fluent_cart/checkout/cart_updated', [ 'cart' => $cart, ]), 'fragments' => $fragments ]; } public function patchCheckoutData() { $cart = CartHelper::getCart(); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $allData = App::request()->all(); $dataKey = sanitize_text_field((string) Arr::get($allData, 'data_key')); $dataValue = sanitize_text_field((string) Arr::get($allData, 'data_value')); if ($dataKey) { $allData[$dataKey] = $dataValue; } $allData = AddressHelper::maybePushAddressDataForCheckout($allData, 'billing'); if (Arr::get($allData, 'ship_to_different') === 'yes') { $allData = AddressHelper::maybePushAddressDataForCheckout($allData, 'shipping'); } else { $allData = AddressHelper::mergeBillingWithShipping($allData); } $validKeys = [ 'ship_to_different' => 'form_data.ship_to_different', 'billing_email' => 'form_data.billing_email', 'billing_address_id' => 'form_data.billing_address_id', 'shipping_address_id' => 'form_data.shipping_address_id', 'billing_company' => 'form_data.billing_company', 'order_notes' => 'form_data.order_notes', 'shipping_method_id' => 'shipping_data.shipping_method_id', '_fct_pay_method' => 'form_data._fct_pay_method', 'billing_company_name' => 'form_data.billing_company_name', 'billing_legal_registration_id' => 'form_data.billing_legal_registration_id', 'is_business' => 'form_data.is_business', 'fct_billing_tax_id' => 'tax_data.vat_number', 'fct_vat_declaration_note' => 'tax_data.declaration_note', ]; $addressFieldKeys = [ 'full_name', 'country', 'address_1', 'address_2', 'state', 'city', 'postcode', 'company_name', 'phone' ]; foreach ($addressFieldKeys as $addressFieldKey) { $validKeys['billing_' . $addressFieldKey] = 'form_data.billing_' . $addressFieldKey; $validKeys['shipping_' . $addressFieldKey] = 'form_data.shipping_' . $addressFieldKey; } $prevFlatData = []; $prevCheckoutData = $cart->checkout_data; foreach ($validKeys as $dataName => $dataPath) { $prevFlatData[$dataName] = Arr::get($prevCheckoutData, $dataPath, null); } // $prevFlatData = array_filter($prevFlatData); $prevFlatData = array_filter($prevFlatData, function ($value) { return $value !== null; }); $validData = Arr::only($allData, array_keys($validKeys)); $normalizeData = []; foreach ($validData as $key => $value) { $prevValue = Arr::get($prevFlatData, $key, null); if ($prevValue != $value) { $normalizeData[$key] = $value; } } // Force-check the explicit data_key field — bulk POST may have already stored it as a side-effect. if ($dataKey && isset($validKeys[$dataKey]) && !isset($normalizeData[$dataKey])) { $storedValue = (string) Arr::get($prevCheckoutData, $validKeys[$dataKey], ''); if ($storedValue !== $dataValue) { $normalizeData[$dataKey] = $dataValue; } } if (!$normalizeData) { return [ 'message' => __('No changes detected', 'fluent-cart') ]; } $normalizeData = $this->normalizeCheckoutChangeData($normalizeData, $allData); // Validate these data and filter out invalid data points and also push the // address data into this array. $checkoutData = $cart->checkout_data; foreach ($normalizeData as $normalizeKey => $normalizeValue) { Arr::set($checkoutData, $validKeys[$normalizeKey], $normalizeValue); } $oldTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $fillData = [ 'checkout_data' => $checkoutData, 'cart_data' => $cart->cart_data, 'hook_changes' => [ 'shipping' => false, 'tax' => false ] ]; $fillData = apply_filters('fluent_cart/checkout/before_patch_checkout_data', $fillData, [ 'cart' => $cart, 'prev_data' => $prevFlatData, 'changes' => $normalizeData, 'all_data' => $allData ]); $hookChanges = Arr::get($fillData, 'hook_changes', []); unset($fillData['hook_changes']); if (isset($normalizeData['billing_email'])) { $cart->email = $normalizeData['billing_email']; } if (isset($normalizeData['billing_full_name'])) { $fullName = $normalizeData['billing_full_name']; $nameParts = explode(' ', $fullName); $cart->first_name = array_shift($nameParts); $cart->last_name = implode(' ', $nameParts); } $cart->fill($fillData); $newTaxTotal = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $taxAmountChanged = $oldTaxTotal !== $newTaxTotal; $cart->clearFeeCache(); $cart = CartHelper::getCart(); $sanitizedUtmData = UtmHelper::getUtmDataOfRequest(); $cart->utm_data = $sanitizedUtmData; $cart->save(); $fragments = []; $cartRender = (new CheckoutRenderer($cart)); $modalCheckoutRender = (new ModalCheckoutRenderer($cart)); $enableModalCheckout = App::request()->get('modal_checkout'); if (!empty($hookChanges['shipping'])) { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-shipping-methods-wrapper]', 'content' => $cartRender->getFragment('shipping_methods'), 'type' => 'replace' ]; } if (array_filter($hookChanges)) { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $cartRender->getFragment('cart_summary_fragment'), 'type' => 'replace' ]; if ($taxAmountChanged || !empty($hookChanges['shipping'])) { // also update the payment methods if ($enableModalCheckout === 'yes') { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $modalCheckoutRender->getFragment('payment_methods'), 'type' => 'replace' ]; } else { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-payment-methods]', 'content' => $cartRender->getFragment('payment_methods'), 'type' => 'replace' ]; } $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary-group]', 'content' => $modalCheckoutRender->getFragment('summary_group'), 'type' => 'replace' ]; $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary]', 'content' => $modalCheckoutRender->getFragment('checkout_summary'), 'type' => 'replace' ]; } } // Re-render cart summary on payment method change (fees may depend on payment method) if (isset($normalizeData['_fct_pay_method'])) { $hasSummaryFragment = false; foreach ($fragments as $fragment) { if (Arr::get($fragment, 'selector') === '[data-fluent-cart-checkout-page-cart-items-wrapper]') { $hasSummaryFragment = true; break; } } if (!$hasSummaryFragment) { $fragments[] = [ 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]', 'content' => $cartRender->getFragment('cart_summary_fragment'), 'type' => 'replace' ]; $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary-group]', 'content' => $modalCheckoutRender->getFragment('summary_group'), 'type' => 'replace' ]; $fragments[] = [ 'selector' => '[data-fct-modal-checkout-summary]', 'content' => $modalCheckoutRender->getFragment('checkout_summary'), 'type' => 'replace' ]; } } if (($dataKey === 'billing_address_id' || $dataKey === 'shipping_address_id') && $dataValue !== '') { // get current customer $customer = CustomerResource::getCurrentCustomer(); $type = $dataKey === 'billing_address_id' ? 'billing' : 'shipping'; $requiredShipping = $cart->requireShipping(); $config = [ 'type' => $type, 'product_type' => $requiredShipping ? 'physical' : 'digital', 'with_shipping' => $requiredShipping ]; if ($type === 'billing') { $config['with_shipping'] = Arr::get($allData, 'ship_to_different', 'no') !== 'yes'; $config['billing_address_id'] = $dataValue; } else { $config['shipping_address_id'] = $dataValue; } $addresses = AddressHelper::getCustomerValidatedAddresses($config, $customer); $address = AddressHelper::getPrimaryAddress($addresses, $config, $customer, $type); $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements( $type, Arr::get($config, 'product_type'), Arr::get($config, 'with_shipping') ); ob_start(); (new AddressSelectRenderer($addresses, $address, $requirementsFields, $type))->renderAddressInfo(); $addressRender = ob_get_clean(); $fragments[] = [ 'selector' => '#' . $type . '_address_wrapper [data-fluent-cart-checkout-page-form-address-info-wrapper]', 'content' => $addressRender, 'type' => 'replace' ]; } $fragments = apply_filters('fluent_cart/checkout/after_patch_checkout_data_fragments', $fragments, [ 'cart' => $cart, 'changes' => $normalizeData ]); return [ 'message' => __('Data saved successfully', 'fluent-cart'), 'changes' => $normalizeData, 'fragments' => $fragments, 'cart' => $cart ]; } public function handleOrderBumpRequest() { $requestData = App::request()->all(); $cart = CartHelper::getCart(); if (!$cart) { return new \WP_Error('no_cart', __('No active cart found', 'fluent-cart')); } $checkoutData = $cart->checkout_data; if (!empty($checkoutData['upgrade_data']) || !empty($checkoutData['is_locked'])) { return new \WP_Error('invalid_request', __('This cart is locked or already has an upgrade applied.', 'fluent-cart')); } $upgradeFromVariationId = (int) Arr::get($requestData, 'upgrade_form', 0); $targetVariationId = (int) Arr::get($requestData, 'upgrade_to', 0); $bumpId = (int) Arr::get($requestData, 'bump_id', 0); if ($bumpId) { $response = new \WP_Error('invalid_bump', __('Could not apply item at this time.', 'fluent-cart')); return apply_filters('fluent_cart/apply_order_bump', $response, [ 'bump_id' => $bumpId, 'cart' => $cart, 'request_data' => $requestData ]); } if (!$upgradeFromVariationId || !$targetVariationId) { return new \WP_Error('invalid_request', __('Invalid upgrade request.', 'fluent-cart')); } $productVariation = ProductVariation::query()->find($targetVariationId); if (!$productVariation || !$productVariation->canPurchase()) { return new \WP_Error('invalid_variation', __('The selected product variation is not available for purchase.', 'fluent-cart')); } $cart->removeItem($upgradeFromVariationId); $cart = $cart->addByVariation($productVariation, [ 'quantity' => 1, 'append' => false ]); if (is_wp_error($cart)) { return $cart; } $isUpgraded = Arr::get($requestData, 'is_upgraded') === 'yes'; if ($isUpgraded) { $checkoutData['order_bump'] = [ 'upgraded_from' => $upgradeFromVariationId, 'upgraded_to' => $targetVariationId ]; $existingActions = Arr::get($checkoutData, '__on_success_actions__', []); if (!is_array($existingActions)) { $existingActions = []; } $existingActions[] = 'fluent_cart/order_bump_succeed'; $checkoutData['__on_success_actions__'] = $existingActions; } else { unset($checkoutData['order_bump']); $existingActions = Arr::get($checkoutData, '__on_success_actions__', []); if (!is_array($existingActions)) { $existingActions = []; } if (!empty($existingActions) && in_array('fluent_cart/order_bump_succeed', $existingActions)) { $existingActions = array_diff($existingActions, ['fluent_cart/order_bump_succeed']); $existingActions = array_values($existingActions); } if (is_array($existingActions)) { $existingActions = array_filter($existingActions, function ($action) { return $action !== 'fluent_cart/order_bump_succeed'; }); } $checkoutData['__on_success_actions__'] = $existingActions; } $cart->checkout_data = $checkoutData; $cart->save(); do_action('fluent_cart/checkout/cart_amount_updated', [ 'cart' => $cart ]); return [ 'message' => $isUpgraded ? __('Item has been applied successfully', 'fluent-cart') : __('Item has been reverted successfully', 'fluent-cart') ]; } public function normalizeCheckoutChangeData($changedData, $allData): array { $sanitizedData = []; $errors = []; foreach ($changedData as $dataKey => $dataValue) { if (in_array($dataKey, ['billing_full_name', 'shipping_full_name'])) { $sanitizedData[$dataKey] = sanitize_text_field($dataValue); } else if (in_array($dataKey, ['billing_email', 'shipping_email'])) { if (empty($dataValue)) { $sanitizedData[$dataKey] = ''; } else { $sanitizedData[$dataKey] = sanitize_email($dataValue ?? ''); } } else if (in_array($dataKey, ['billing_company_name', 'shipping_company_name', 'billing_legal_registration_id'])) { $sanitizedData[$dataKey] = sanitize_text_field($dataValue); } else if ($dataKey === 'is_business') { $sanitizedData[$dataKey] = $dataValue === 'yes' ? 'yes' : 'no'; } else if ($dataKey === 'fct_vat_declaration_note') { $sanitizedData[$dataKey] = mb_substr(sanitize_text_field($dataValue), 0, 255); } else if ($dataKey === 'billing_state' && !empty($dataValue)) { $billingCountry = Arr::get($allData, 'billing_country'); $states = LocalizationManager::getInstance()->statesOptions($billingCountry); $countryStates = array_values(array_column($states, 'value')); if (!empty($states) && !in_array($dataValue, $countryStates)) { $errors[$dataKey] = __('Invalid state code.', 'fluent-cart'); $sanitizedData[$dataKey] = null; } else { $sanitizedData[$dataKey] = $dataValue; } } else if ($dataKey === 'shipping_state' && !empty($dataValue)) { $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes'); $shippingCountry = $shipToDifferent ? Arr::get($allData, 'shipping_country') : Arr::get($allData, 'billing_country'); $states = LocalizationManager::getInstance()->statesOptions($shippingCountry); if (!empty($states) && !in_array($dataValue, array_column($states, 'value'))) { $errors[$dataKey] = __('Invalid state code.', 'fluent-cart'); $sanitizedData[$dataKey] = null; } else { $sanitizedData[$dataKey] = $dataValue; } } else if ($dataKey === '_fct_pay_method') { $value = sanitize_text_field($dataValue); $methods = PaymentMethods::getActiveMeta(); $methods = array_column($methods, 'route'); if (!in_array($value, $methods)) { $value = null; $errors[$dataKey] = __('Invalid payment method.', 'fluent-cart'); } $sanitizedData[$dataKey] = $value; } else if ($dataKey === 'shipping_method_id') { $shipToDifferent = ((Arr::get($allData, 'ship_to_different', 'no') === 'yes') == 'yes'); $countryKey = $shipToDifferent ? 'shipping_country' : 'billing_country'; $stateKey = $shipToDifferent ? 'shipping_state' : 'billing_state'; $shippingCountry = Arr::has($changedData, $countryKey) ? Arr::get($changedData, $countryKey) : Arr::get($allData, $countryKey); $shippingState = Arr::has($changedData, $stateKey) ? Arr::get($changedData, $stateKey) : Arr::get($allData, $stateKey); $availableShippingMethods = AddressHelper::getShippingMethods($shippingCountry, $shippingState); $found = false; foreach ($availableShippingMethods as $method) { if ($method->id == $dataValue) { $found = true; break; } } if (!$found) { $errors[$dataKey] = __('Invalid shipping method.', 'fluent-cart'); $sanitizedData[$dataKey] = null; } else { $sanitizedData[$dataKey] = sanitize_text_field($dataValue); } } else { $sanitizedData[$dataKey] = sanitize_text_field($dataValue); } } return $sanitizedData; } private function pushAddressData($data, $type = 'billing') { return AddressHelper::maybePushAddressDataForCheckout($data, $type); } public function getProductModalView() { $productId = App::request()->getSafe('product_id', 'intval'); $product = Product::query()->find($productId); if (!$product) { return $this->sendError([ 'message' => __('Product not found', 'fluent-cart') ]); } ob_start(); (new ProductModalRenderer($product))->render(); $view = ob_get_clean(); return [ 'view' => $view ]; } }