array( 'woocommerce_after_cart_totals', 10 ), 'before_checkout' => array( 'woocommerce_proceed_to_checkout', 5 ), 'after_checkout' => array( 'woocommerce_proceed_to_checkout', 30 ), 'top' => array( 'woocommerce_before_cart', 10 ), ); $pos = DevisQR_Settings::get( 'cart_button_position' ); $hook = isset( $map[ $pos ] ) ? $map[ $pos ][0] : ''; $prio = isset( $map[ $pos ] ) ? $map[ $pos ][1] : 10; /** * Filter the WooCommerce hook used to print the cart button ('' = none, use the shortcode). * * @param string $hook Hook name. * @param string $pos Configured position. */ $hook = apply_filters( 'devisqr_cart_button_hook', $hook, $pos ); if ( $hook ) { add_action( $hook, array( $this, 'render_quote_button' ), $prio ); } } /* ------------------------------------------------------------------ */ /* Assets */ /* ------------------------------------------------------------------ */ /** * Enqueue cart assets. */ public function enqueue_assets() { if ( ! is_cart() && ! is_checkout() ) { return; } if ( ! is_cart() ) { self::enqueue_style(); return; } self::enqueue_front_assets(); // Block cart: render the button through the WooCommerce Blocks slot. if ( $this->cart_page_uses_block() && wp_script_is( 'wc-blocks-checkout', 'registered' ) ) { wp_enqueue_script( 'devisqr-cart-block', DEVISQR_PLUGIN_URL . 'assets/js/devisqr-cart-block.js', array( 'devisqr-cart', 'wc-blocks-checkout', 'wp-plugins', 'wp-element', 'wp-i18n' ), DEVISQR_VERSION, true ); wp_set_script_translations( 'devisqr-cart-block', 'nuxwp-devis', DEVISQR_PLUGIN_DIR . 'languages' ); } } /** * Stylesheet + admin-driven inline CSS. */ public static function enqueue_style() { if ( wp_style_is( 'devisqr-cart', 'enqueued' ) ) { return; } wp_enqueue_style( 'devisqr-cart', DEVISQR_PLUGIN_URL . 'assets/css/devisqr-cart.css', array(), DEVISQR_VERSION ); wp_add_inline_style( 'devisqr-cart', DevisQR_Buttons::inline_css() ); } /** * Style + shared script + localized data (cart page and product page). */ public static function enqueue_front_assets() { self::enqueue_style(); if ( wp_script_is( 'devisqr-cart', 'enqueued' ) ) { return; } wp_enqueue_script( 'devisqr-cart', DEVISQR_PLUGIN_URL . 'assets/js/devisqr-cart.js', array( 'jquery', 'wp-i18n' ), DEVISQR_VERSION, true ); wp_set_script_translations( 'devisqr-cart', 'nuxwp-devis', DEVISQR_PLUGIN_DIR . 'languages' ); $settings = DevisQR_Settings::resolved(); $min_total = DevisQR_Helpers::min_cart_total(); wp_localize_script( 'devisqr-cart', 'devisqrCart', array( 'restUrl' => esc_url_raw( rest_url( DevisQR_API::NAMESPACE . '/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'ajaxNonce' => wp_create_nonce( 'devisqr_submit' ), 'minTotal' => $min_total, 'minTotalText' => $min_total > 0 ? DevisQR_Helpers::price_text( $min_total ) : '', 'cartButtonEnabled' => (bool) DevisQR_Settings::get( 'cart_button_enabled' ), 'cartWrapClass' => DevisQR_Buttons::wrap_class( 'cart', 'devisqr-quote-wrap--block' ), 'cartButtonText' => DevisQR_Buttons::label( 'cart' ), 'currency' => DevisQR_Helpers::js_currency(), 'textOverrides' => DevisQR_Texts::overrides_for_js(), 'isLoggedIn' => is_user_logged_in(), 'userAccess' => DevisQR_Helpers::user_access(), 'loginUrl' => wc_get_page_permalink( 'myaccount' ), 'cartUrl' => wc_get_cart_url(), 'accountUrl' => wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() ), 'messageEnabled' => ! empty( $settings['customer_message_enabled'] ), 'deniedHtml' => wp_kses_post( __( 'Your account is not allowed to request quotes.', 'nuxwp-devis' ) . ' ' . DevisQR_Helpers::contact_link() ), ) ); } /** * Is the cart page built with the Cart block (vs the [woocommerce_cart] shortcode)? * * @return bool */ private function cart_page_uses_block() { if ( function_exists( 'has_block' ) && has_block( 'woocommerce/cart' ) ) { return true; } $cart_page_id = wc_get_page_id( 'cart' ); return $cart_page_id > 0 && function_exists( 'has_block' ) && has_block( 'woocommerce/cart', $cart_page_id ); } /** * Body class while the cart holds an accepted quote. * * @param array $classes Classes. * @return array */ public function body_class( $classes ) { if ( ( is_cart() || is_checkout() ) && self::cart_has_quote_items() ) { $classes[] = 'devisqr-locked-cart'; } return $classes; } /* ------------------------------------------------------------------ */ /* Button */ /* ------------------------------------------------------------------ */ /** * Shortcode [devisqr_button]. * * @return string */ public function shortcode_button() { if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return ''; } self::enqueue_front_assets(); ob_start(); $this->render_quote_button(); return ob_get_clean(); } /** * Render the quote button (classic cart). */ public function render_quote_button() { static $rendered = false; if ( $rendered || ! DevisQR_Settings::get( 'cart_button_enabled' ) ) { return; } if ( ! WC()->cart || WC()->cart->is_empty() || self::cart_has_quote_items() ) { return; } $rendered = true; $cart_total = (float) WC()->cart->get_subtotal() + (float) WC()->cart->get_subtotal_tax(); $min_total = DevisQR_Helpers::min_cart_total(); $enabled = $min_total <= 0 || $cart_total >= $min_total; DevisQR_Buttons::render( 'cart', $enabled, array( 'data-min-total' => $min_total ) ); } /* ------------------------------------------------------------------ */ /* AJAX fallbacks */ /* ------------------------------------------------------------------ */ /** * AJAX: submit (mirrors the REST callback). */ public function ajax_submit_quote() { check_ajax_referer( 'devisqr_submit', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'nuxwp-devis' ) ) ); } $request = new WP_REST_Request( 'POST' ); if ( isset( $_POST['customer_note'] ) ) { $request->set_param( 'customer_note', sanitize_textarea_field( wp_unslash( $_POST['customer_note'] ) ) ); } $api = new DevisQR_API(); $result = $api->submit_quote( $request ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( $result->get_data() ); } /** * AJAX: load an accepted quote into the cart. */ public function ajax_add_to_cart() { check_ajax_referer( 'devisqr_submit', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'nuxwp-devis' ) ) ); } $quote_id = isset( $_POST['quote_id'] ) ? absint( wp_unslash( $_POST['quote_id'] ) ) : 0; $quote = $quote_id ? DevisQR_Database::get_quote( $quote_id ) : null; if ( ! $quote || (int) $quote->user_id !== get_current_user_id() ) { wp_send_json_error( array( 'message' => __( 'Quote not found.', 'nuxwp-devis' ) ) ); } if ( 'converted' === $quote->status ) { wp_send_json_error( array( 'message' => __( 'This quote has already been converted into an order.', 'nuxwp-devis' ) ) ); } if ( 'accepted' !== $quote->status ) { wp_send_json_error( array( 'message' => __( 'This quote has not been accepted yet.', 'nuxwp-devis' ) ) ); } if ( DevisQR_Helpers::is_expired( $quote ) ) { DevisQR_Database::update_quote( $quote->id, array( 'status' => 'expired' ) ); wp_send_json_error( array( 'message' => __( 'This quote has expired. Please submit a new quote request.', 'nuxwp-devis' ) ) ); } $added = self::load_quote_into_cart( $quote ); if ( is_wp_error( $added ) ) { wp_send_json_error( array( 'message' => $added->get_error_message() ) ); } wp_send_json_success( array( 'success' => true, /* translators: %d: number of products */ 'message' => sprintf( _n( '%d product added to the cart.', '%d products added to the cart.', $added, 'nuxwp-devis' ), $added ), 'redirect_url' => wc_get_checkout_url(), ) ); } /** * Empty the cart and load the quote lines with frozen prices. * * @param object $quote Quote row. * @return int|WP_Error Number of lines added, or error (cart emptied) when a line could not be added. */ public static function load_quote_into_cart( $quote ) { $items = DevisQR_Database::get_quote_items( $quote->id ); if ( empty( $items ) ) { return new WP_Error( 'empty_quote', __( 'This quote has no products.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } WC()->cart->empty_cart(); wc_clear_notices(); $added = 0; foreach ( $items as $item ) { $quantity = (float) $item->quantity; $variation = array(); if ( $item->meta_data ) { $meta = json_decode( $item->meta_data, true ); if ( ! empty( $meta['variation'] ) ) { $variation = $meta['variation']; } } $cart_item_data = array( 'devisqr_quote_id' => (int) $quote->id, 'devisqr_quote_number' => $quote->quote_number, 'devisqr_quote_price' => (float) $item->line_total_ttc / max( $quantity, 1 ), 'devisqr_quote_price_ht' => (float) $item->line_total_ht / max( $quantity, 1 ), 'devisqr_quote_discount' => isset( $item->discount_percent ) ? (float) $item->discount_percent : 0, 'devisqr_original_price' => (float) $item->unit_price_ttc, 'devisqr_original_price_ht' => (float) $item->unit_price_ht, ); if ( WC()->cart->add_to_cart( (int) $item->product_id, $quantity, (int) $item->variation_id, $variation, $cart_item_data ) ) { $added++; } else { // All or nothing: never let the customer pay a partial quote at negotiated prices. WC()->cart->empty_cart(); wc_clear_notices(); return new WP_Error( 'cart_error', /* translators: 1: product name, 2: "contact us" */ sprintf( __( 'ā%1$sā is no longer available in the requested quantity. Please %2$s so we can update your quote.', 'nuxwp-devis' ), $item->product_name, wp_strip_all_tags( DevisQR_Helpers::contact_link() ) ), array( 'status' => 409 ) ); } } return $added; } /* ------------------------------------------------------------------ */ /* Locked cart */ /* ------------------------------------------------------------------ */ /** * Does the cart hold quote lines? * * @return bool */ public static function cart_has_quote_items() { if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return false; } foreach ( WC()->cart->get_cart() as $item ) { if ( ! empty( $item['devisqr_quote_id'] ) ) { return true; } } return false; } /** * Quote number found in the cart. * * @return string */ private function get_cart_quote_number() { foreach ( WC()->cart->get_cart() as $item ) { if ( ! empty( $item['devisqr_quote_number'] ) ) { return $item['devisqr_quote_number']; } } return ''; } /** * "Quotes cannot be modified" message with contact link. * * @return string HTML. */ private function locked_message() { return sprintf( /* translators: %s: "contact us" link */ __( 'The products and quantities of a quote cannot be modified. For any change, please %s.', 'nuxwp-devis' ), DevisQR_Helpers::contact_link() ); } /** * Hard server-side quantity lock (classic form, Store API, third-party carts). * * @param string $cart_item_key Key. * @param float $quantity New qty. * @param float $old_quantity Old qty. * @param WC_Cart $cart Cart. */ public function lock_quantity_after_update( $cart_item_key, $quantity, $old_quantity, $cart ) { if ( (float) $quantity === (float) $old_quantity ) { return; } if ( isset( $cart->cart_contents[ $cart_item_key ] ) && ! empty( $cart->cart_contents[ $cart_item_key ]['devisqr_quote_id'] ) ) { $cart->cart_contents[ $cart_item_key ]['quantity'] = $old_quantity; wc_add_notice( wp_kses_post( $this->locked_message() ), 'error' ); } } /** * Badges on quote lines. * * @param array $item_data Data. * @param array $cart_item Cart item. * @return array */ public function display_quote_badge( $item_data, $cart_item ) { if ( ! empty( $cart_item['devisqr_quote_number'] ) ) { $item_data[] = array( 'key' => __( 'Quote', 'nuxwp-devis' ), 'value' => $cart_item['devisqr_quote_number'], ); } if ( ! empty( $cart_item['devisqr_quote_discount'] ) && (float) $cart_item['devisqr_quote_discount'] > 0 ) { $value = '-' . esc_html( DevisQR_Helpers::percent_text( $cart_item['devisqr_quote_discount'] ) ) . ''; $orig_key = WC()->cart->display_prices_including_tax() ? 'devisqr_original_price' : 'devisqr_original_price_ht'; if ( ! empty( $cart_item[ $orig_key ] ) && (float) $cart_item[ $orig_key ] > 0 ) { $value .= ' ' . wc_price( (float) $cart_item[ $orig_key ] ) . ''; } $item_data[] = array( 'key' => __( 'Discount', 'nuxwp-devis' ), 'value' => $value, 'display' => $value, ); } return $item_data; } /** * No remove link on quote lines. * * @param string $link Link. * @param string $cart_item_key Key. * @return string */ public function lock_remove_link( $link, $cart_item_key ) { $cart = WC()->cart->get_cart(); if ( isset( $cart[ $cart_item_key ] ) && ! empty( $cart[ $cart_item_key ]['devisqr_quote_id'] ) ) { return ''; } return $link; } /** * Read-only quantity on quote lines. * * @param string $product_quantity HTML. * @param string $cart_item_key Key. * @param array $cart_item Item. * @return string */ public function lock_quantity( $product_quantity, $cart_item_key, $cart_item ) { if ( ! empty( $cart_item['devisqr_quote_id'] ) ) { return '' . esc_html( DevisQR_Helpers::qty_text( $cart_item['quantity'] ) ) . ''; } return $product_quantity; } /** * Banner on cart/checkout when paying a quote. */ public function show_quote_cart_notice() { static $shown = false; if ( $shown || ! self::cart_has_quote_items() ) { return; } $shown = true; ?>