calculator = DP_AICOSO_Price_Calculator::get_instance(); $this->init_hooks(); } /** * Initialize hooks. */ private function init_hooks() { // Cart pricing - main hook with high priority. add_action( 'woocommerce_before_calculate_totals', array( $this, 'apply_cart_pricing' ), 9999, 1 ); // Essential cart session hooks to ensure prices persist. add_filter( 'woocommerce_add_cart_item', array( $this, 'add_cart_item' ), 9999, 2 ); add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'get_cart_item_from_session' ), 9999, 2 ); // Mini cart price display fix. add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'refresh_mini_cart_prices' ), 99 ); add_filter( 'woocommerce_widget_cart_item_quantity', array( $this, 'mini_cart_item_price' ), 10, 3 ); // Add shortcode for dynamic price display. add_shortcode( 'dp_aicoso_dynamic_price', array( $this, 'dynamic_price_shortcode' ) ); // Product page display. add_action( 'woocommerce_single_product_summary', array( $this, 'display_bulk_pricing_table' ), 15 ); // Higher priority to ensure our pricing runs after other plugins. add_filter( 'woocommerce_get_price_html', array( $this, 'modify_price_html' ), 999, 2 ); // AJAX handlers. add_action( 'wp_ajax_dp_aicoso_get_bulk_pricing', array( $this, 'ajax_get_bulk_pricing' ) ); add_action( 'wp_ajax_nopriv_dp_aicoso_get_bulk_pricing', array( $this, 'ajax_get_bulk_pricing' ) ); add_action( 'wp_ajax_dp_aicoso_get_product_price', array( $this, 'ajax_get_product_price' ) ); add_action( 'wp_ajax_nopriv_dp_aicoso_get_product_price', array( $this, 'ajax_get_product_price' ) ); // Variation price display. add_filter( 'woocommerce_available_variation', array( $this, 'modify_variation_data' ), 10, 3 ); // Set default quantity input value based on cart quantity. add_filter( 'woocommerce_quantity_input_args', array( $this, 'set_quantity_input_args' ), 10, 2 ); // Coupon hooks - trigger recalculation when coupons are added/removed. add_action( 'woocommerce_applied_coupon', array( $this, 'handle_coupon_change' ) ); add_action( 'woocommerce_removed_coupon', array( $this, 'handle_coupon_change' ) ); } /** * Apply dynamic pricing to cart items. * This is the ONLY method that sets prices in cart. * * @param WC_Cart $cart Cart object. */ public function apply_cart_pricing( $cart ) { // Essential checks. if ( ! $cart || $cart->is_empty() ) { return; } if ( is_admin() && ! wp_doing_ajax() ) { return; } // Prevent recursive calls. if ( self::$is_processing ) { return; } // Set processing flag. self::$is_processing = true; // Process each cart item. foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { if ( ! isset( $cart_item['data'] ) || ! $cart_item['data'] ) { continue; } $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Apply dynamic pricing (the rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); // Set price if calculated. if ( false !== $dynamic_price && is_numeric( $dynamic_price ) ) { $product->set_price( $dynamic_price ); } } // Clear processing flag. self::$is_processing = false; } /** * Apply pricing when item is added to cart. * * @param array $cart_item Cart item data. * @param string $cart_item_key Cart item key. * @return array Modified cart item data. */ public function add_cart_item( $cart_item, $cart_item_key ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Apply dynamic pricing (the rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { $product->set_price( $dynamic_price ); } } return $cart_item; } /** * Restore pricing when cart is loaded from session. * * @param array $cart_item Cart item data. * @param array $values Session values. * @return array Modified cart item data. */ public function get_cart_item_from_session( $cart_item, $values ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Apply dynamic pricing (the rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { $product->set_price( $dynamic_price ); } } return $cart_item; } /** * Get dynamic display price for products (shop/product pages). * Carefully avoids interfering with cart calculations. * * @param float $price Original price. * @param WC_Product $product Product object. * @return float Dynamic price. */ public function get_dynamic_display_price( $price, $product ) { // CRITICAL: Skip if we're processing cart to prevent interference. if ( self::$is_processing ) { return $price; } // Skip in admin. if ( is_admin() && ! wp_doing_ajax() ) { return $price; } // Skip if we're in cart or checkout. if ( is_cart() || is_checkout() ) { return $price; } // Skip if this is happening during cart calculations. if ( did_action( 'woocommerce_before_calculate_totals' ) && ! did_action( 'woocommerce_after_calculate_totals' ) ) { return $price; } // Skip if no price. if ( '' === $price || null === $price ) { return $price; } // Skip for variable and grouped products (they handle their own pricing). if ( $product->is_type( array( 'variable', 'grouped' ) ) ) { return $price; } // ONLY apply dynamic pricing in AJAX context or for shop/archive pages. // For product pages, let modify_price_html handle the display. if ( ! wp_doing_ajax() && is_product() ) { return $price; } // For AJAX requests, check if we have a specific quantity. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only operation if ( wp_doing_ajax() && isset( $_POST['quantity'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only operation $quantity = intval( $_POST['quantity'] ); } else { // Default to quantity 1 for shop/archive pages. $quantity = 1; } // Calculate dynamic price. $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); return ( false !== $dynamic_price ) ? $dynamic_price : $price; } /** * Check if product is currently in cart. * * @param WC_Product $product Product to check. * @return bool True if in cart. */ private function is_product_in_cart( $product ) { if ( ! WC()->cart || WC()->cart->is_empty() ) { return false; } $product_id = $product->get_id(); $parent_id = $product->get_parent_id(); foreach ( WC()->cart->get_cart() as $cart_item ) { if ( isset( $cart_item['product_id'] ) ) { if ( $cart_item['product_id'] == $product_id ) { return true; } if ( $parent_id && $cart_item['product_id'] == $parent_id ) { return true; } if ( isset( $cart_item['variation_id'] ) && $cart_item['variation_id'] == $product_id ) { return true; } } } return false; } /** * Modify product price HTML for display. * * @param string $price_html Price HTML. * @param WC_Product $product Product object. * @return string Modified price HTML. */ public function modify_price_html( $price_html, $product ) { // Skip in admin. if ( is_admin() && ! wp_doing_ajax() ) { return $price_html; } // Dynamic pricing rules will check their own disable settings for sale products. // Skip during cart processing. if ( self::$is_processing ) { return $price_html; } // Skip REST API requests to prevent cart calculation interference. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return $price_html; } // Skip if this is happening during cart calculations. if ( did_action( 'woocommerce_before_calculate_totals' ) && ! did_action( 'woocommerce_after_calculate_totals' ) ) { return $price_html; } // Skip in cart and checkout pages. if ( is_cart() || is_checkout() ) { return $price_html; } // Handle variable products separately. if ( $product->is_type( 'variable' ) ) { return $this->modify_variable_product_price_html( $price_html, $product ); } // Skip grouped products. if ( $product->is_type( 'grouped' ) ) { return $price_html; } // Process simple products and variations. if ( ! $product->is_type( 'simple' ) && ! $product->is_type( 'variation' ) ) { return $price_html; } // ON SINGLE PRODUCT PAGES: Handle main product and related products differently. if ( is_product() ) { global $woocommerce_loop, $post; $regular_price = $product->get_regular_price(); if ( empty( $regular_price ) ) { return $price_html; } // Check if we're in a loop (related, cross-sells, etc.). $in_loop = ! empty( $woocommerce_loop['name'] ); // Determine if this is the main product (including nav bar instances). $is_main_product = false; $product_id = $product->get_id(); // Check if this product ID matches the main post ID. if ( $post && $post->ID && $product_id == $post->ID ) { // This product IS the main product being viewed. // Apply to both main display AND nav bar (which shows same product). $is_main_product = true; } if ( $is_main_product ) { // MAIN PRODUCT (including nav bar): Use cart quantity if available. $cart_quantity = $this->get_product_cart_quantity( $product ); $display_quantity = $cart_quantity > 0 ? $cart_quantity : 1; // Calculate dynamic price with actual quantity. $dynamic_price = $this->calculator->calculate_product_price( $product, $display_quantity ); // Apply discount if available. if ( false !== $dynamic_price && is_numeric( $dynamic_price ) && $dynamic_price > 0 && $dynamic_price < $regular_price ) { $price_html = wc_format_sale_price( $regular_price, $dynamic_price ); // Add cart quantity indicator if applicable (but not in nav bar). if ( $display_quantity > 1 && ! $in_loop ) { $price_html .= sprintf( ' %s', /* translators: %d: number of items in cart */ sprintf( __( '(for %d items in cart)', 'aicoso-dynamic-pricing-for-woocommerce' ), $display_quantity ) ); } } } else { // RELATED/OTHER PRODUCTS: Check their own cart quantities. $cart_quantity = $this->get_product_cart_quantity( $product ); if ( $cart_quantity > 0 ) { // This related product IS in cart - use its cart quantity. $display_quantity = $cart_quantity; } else { // Not in cart - use quantity 1 for price display. $display_quantity = 1; } // Calculate dynamic price. $dynamic_price = $this->calculator->calculate_product_price( $product, $display_quantity ); // Only show discount if it actually applies at this quantity. if ( false !== $dynamic_price && is_numeric( $dynamic_price ) && $dynamic_price > 0 && $dynamic_price < $regular_price ) { $price_html = wc_format_sale_price( $regular_price, $dynamic_price ); } // If no discount at quantity 1, show regular price (no strikethrough). } return $price_html; } // FOR SHOP/ARCHIVE/LOOP CONTEXTS: Apply dynamic pricing. // This includes shop pages, category pages, search results, widgets, etc. $regular_price = $product->get_regular_price(); if ( empty( $regular_price ) ) { return $price_html; } // Check if product is in cart to determine quantity. $cart_quantity = $this->get_product_cart_quantity( $product ); $display_quantity = $cart_quantity > 0 ? $cart_quantity : 1; // Calculate dynamic price. $dynamic_price = $this->calculator->calculate_product_price( $product, $display_quantity ); // Apply discount if available. if ( false !== $dynamic_price && is_numeric( $dynamic_price ) && $dynamic_price > 0 ) { if ( $dynamic_price < $regular_price ) { // Show sale price format. $price_html = wc_format_sale_price( $regular_price, $dynamic_price ); // Add cart quantity indicator if product is in cart with quantity > 1. if ( $cart_quantity > 1 ) { $price_html .= sprintf( ' %s', /* translators: %d: quantity in cart */ sprintf( __( '(%d in cart)', 'aicoso-dynamic-pricing-for-woocommerce' ), $cart_quantity ) ); } } else { // No discount, just show regular price. $price_html = wc_price( $regular_price ); } } return $price_html; } /** * Modify variable product price HTML to show dynamic pricing range. * * @param string $price_html Price HTML. * @param WC_Product_Variable $product Variable product object. * @return string Modified price HTML. */ private function modify_variable_product_price_html( $price_html, $product ) { // Get all variations. $variations = $product->get_available_variations(); if ( empty( $variations ) ) { return $price_html; } $min_price = PHP_INT_MAX; $max_price = 0; $min_regular = PHP_INT_MAX; $max_regular = 0; $has_discount = false; // Calculate price range with dynamic pricing. foreach ( $variations as $variation_data ) { $variation = wc_get_product( $variation_data['variation_id'] ); if ( ! $variation ) { continue; } $regular_price = $variation->get_regular_price(); if ( empty( $regular_price ) ) { continue; } // Track regular price range. $min_regular = min( $min_regular, $regular_price ); $max_regular = max( $max_regular, $regular_price ); // Calculate dynamic price for this variation. $dynamic_price = $this->calculator->calculate_product_price( $variation, 1 ); if ( false !== $dynamic_price && is_numeric( $dynamic_price ) && $dynamic_price > 0 ) { $min_price = min( $min_price, $dynamic_price ); $max_price = max( $max_price, $dynamic_price ); if ( $dynamic_price < $regular_price ) { $has_discount = true; } } else { // No discount, use regular price. $min_price = min( $min_price, $regular_price ); $max_price = max( $max_price, $regular_price ); } } // Build price HTML. if ( $min_price < PHP_INT_MAX && $max_price > 0 ) { if ( $has_discount ) { // Show price range with strikethrough. if ( $min_price == $max_price ) { // All variations same price. if ( $min_regular == $max_regular ) { $price_html = wc_format_sale_price( $min_regular, $min_price ); } else { // Different regular prices but same dynamic price. $price_html = wc_price( $min_price ); } } else { // Price range. $regular_range = wc_format_price_range( $min_regular, $max_regular ); $dynamic_range = wc_format_price_range( $min_price, $max_price ); if ( $min_regular != $min_price || $max_regular != $max_price ) { // Show strikethrough for the range. $price_html = '' . $regular_range . ' ' . $dynamic_range . ''; } else { $price_html = $dynamic_range; } } } } return $price_html; } /** * Display bulk pricing table on product page. */ public function display_bulk_pricing_table() { global $product; if ( ! $product ) { return; } // Skip bulk pricing table for products on sale. // Dynamic pricing rules will check their own disable settings. // Get settings with defaults. $defaults = array( 'show_bulk_table' => true, 'enable_bulk_pricing' => true, ); $settings = get_option( 'dp_aicoso_settings', array() ); $settings = wp_parse_args( $settings, $defaults ); // Check if bulk table should be shown. if ( empty( $settings['show_bulk_table'] ) || empty( $settings['enable_bulk_pricing'] ) ) { return; } // Get bulk pricing table data from calculator (like the working codebase). $bulk_data = $this->calculator->get_bulk_pricing_table( $product->get_id() ); if ( empty( $bulk_data ) ) { return; } // Format data for template. $table_data = $this->format_bulk_pricing_data( $bulk_data, $product ); // Load template. $template_path = DP_AICOSO_PLUGIN_DIR . 'public/partials/bulk-pricing-table.php'; if ( file_exists( $template_path ) ) { include $template_path; } } /** * Get product rules. * * @param int $product_id Product ID. * @return array Array of rule objects. */ private function get_product_rules( $product_id ) { global $wpdb; $rules = array(); // Validate product ID. $product_id = intval( $product_id ); // First, get rule IDs specifically for this product. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Caching handled elsewhere, table name is hardcoded $specific_rule_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT rule_id FROM `' . $wpdb->prefix . 'dp_aicoso_rule_relationships` WHERE object_id = %d AND object_type = %s', $product_id, 'product' ) ); // Then, get rules that apply to ALL products (rules with no relationships). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table names are hardcoded $all_product_rules = $wpdb->get_col( $wpdb->prepare( 'SELECT r.rule_id FROM `' . $wpdb->prefix . 'dp_aicoso_rules` r LEFT JOIN `' . $wpdb->prefix . 'dp_aicoso_rule_relationships` rel ON r.rule_id = rel.rule_id WHERE r.rule_type = %s AND r.rule_status = %s AND rel.rule_id IS NULL', 'product', 'active' ) ); // Combine both sets of rule IDs. $all_rule_ids = array_unique( array_merge( $specific_rule_ids, $all_product_rules ) ); if ( empty( $all_rule_ids ) ) { return $rules; } // Get rules. foreach ( $all_rule_ids as $rule_id ) { // Validate rule ID. $rule_id = intval( $rule_id ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Caching handled elsewhere, table name is hardcoded $rule_data = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM `' . $wpdb->prefix . 'dp_aicoso_rules` WHERE rule_id = %d AND rule_status = %s', $rule_id, 'active' ), ARRAY_A ); if ( $rule_data && 'product' === $rule_data['rule_type'] ) { if ( class_exists( 'DP_AICOSO_Product_Rule' ) ) { $rule = new DP_AICOSO_Product_Rule( $rule_data ); if ( method_exists( $rule, 'load_relationships' ) ) { $rule->load_relationships(); } $rules[] = $rule; } } } return $rules; } /** * Format bulk pricing data for display. * * @param array $bulk_data Raw bulk pricing data. * @param WC_Product $product Product object. * @return array Formatted data for template. */ private function format_bulk_pricing_data( $bulk_data, $product ) { $formatted = array(); // Get the original price, handling variable products. if ( $product->is_type( 'variable' ) ) { // For variable products, use the minimum price. $original_price = $product->get_variation_price( 'min' ); } else { $original_price = $product->get_regular_price(); } // Convert to float to ensure numeric value. $original_price = floatval( $original_price ); // If still no price, return empty array. if ( empty( $original_price ) || $original_price <= 0 ) { return $formatted; } foreach ( $bulk_data as $range ) { // Check if data is already formatted (from get_bulk_pricing_table). if ( isset( $range['quantity'] ) && isset( $range['formatted_price'] ) ) { // Data is already formatted, just pass it through. $formatted[] = array( 'quantity' => $range['quantity'], 'formatted_price' => $range['formatted_price'], 'formatted_savings' => $range['formatted_savings'], 'discount_type' => $range['discount_type'], 'discount_value' => $range['discount_value'], ); } else { // Data needs formatting - this handles the raw data case. // Format quantity range. $quantity = isset( $range['min_quantity'] ) ? $range['min_quantity'] : 1; if ( ! empty( $range['max_quantity'] ) && $range['max_quantity'] > 0 ) { $quantity .= ' - ' . $range['max_quantity']; } else { $quantity .= '+'; } // Calculate discounted price based on discount type. $discounted_price = $original_price; if ( isset( $range['discount_type'] ) && isset( $range['discount_value'] ) ) { if ( 'percentage' === $range['discount_type'] ) { $discounted_price = $original_price * ( 1 - ( floatval( $range['discount_value'] ) / 100 ) ); } elseif ( 'fixed' === $range['discount_type'] ) { $discounted_price = $original_price - floatval( $range['discount_value'] ); } elseif ( 'fixed_price' === $range['discount_type'] ) { // Fixed price - set the price directly to the discount value. $discounted_price = floatval( $range['discount_value'] ); } } elseif ( isset( $range['price'] ) ) { // If price is directly provided, use it. $discounted_price = floatval( $range['price'] ); } // Ensure discounted price is not negative. $discounted_price = max( 0, $discounted_price ); // Calculate savings. $savings = $original_price - $discounted_price; $formatted[] = array( 'quantity' => $quantity, 'formatted_price' => wc_price( $discounted_price ), 'formatted_savings' => wc_price( $savings ), 'discount_type' => isset( $range['discount_type'] ) ? $range['discount_type'] : '', 'discount_value' => isset( $range['discount_value'] ) ? $range['discount_value'] : 0, ); } } return $formatted; } /** * AJAX handler for getting bulk pricing table. */ public function ajax_get_bulk_pricing() { // Verify nonce. check_ajax_referer( 'dp_aicoso_public', 'nonce' ); $product_id = isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0; if ( ! $product_id ) { wp_send_json_error( 'Invalid product ID' ); } $product = wc_get_product( $product_id ); if ( ! $product ) { wp_send_json_error( 'Product not found' ); } // Dynamic pricing rules will check their own disable settings. // Get rules for this product. $rules = $this->get_product_rules( $product_id ); if ( empty( $rules ) ) { wp_send_json_error( 'No pricing rules found' ); } // Build bulk pricing data. $bulk_data = array(); foreach ( $rules as $rule ) { if ( method_exists( $rule, 'has_quantity_ranges' ) && $rule->has_quantity_ranges() ) { if ( method_exists( $rule, 'get_bulk_pricing_table' ) ) { $table_data = $rule->get_bulk_pricing_table( $product_id ); if ( ! empty( $table_data ) ) { $bulk_data = array_merge( $bulk_data, $table_data ); } } } } if ( empty( $bulk_data ) ) { wp_send_json_error( 'No bulk pricing found' ); } // Generate HTML. ob_start(); $template_path = DP_AICOSO_PLUGIN_DIR . 'public/partials/bulk-pricing-table.php'; if ( file_exists( $template_path ) ) { include $template_path; } $html = ob_get_clean(); wp_send_json_success( array( 'html' => $html, 'data' => $bulk_data, ) ); } /** * Modify Store API product price. * * @param string $price_html Price HTML. * @param array $product_data Product data. * @param WC_Product $product Product object. * @return string Modified price. */ public function store_api_product_price( $price_html, $product_data, $product ) { // This is for the Store API used by Blocks. if ( ! $product ) { return $price_html; } // Dynamic pricing rules will check their own disable settings. // Get quantity from cart. $quantity = 1; if ( WC()->cart && ! WC()->cart->is_empty() ) { foreach ( WC()->cart->get_cart() as $cart_item ) { if ( $cart_item['product_id'] == $product->get_id() ) { $quantity = $cart_item['quantity']; break; } } } $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { return wc_price( $dynamic_price ); } return $price_html; } /** * Register Blocks integration. */ public function register_blocks_integration() { // Hook into the Store API cart schema. add_filter( 'woocommerce_store_api_cart_schema', array( $this, 'extend_cart_schema' ) ); add_filter( 'woocommerce_store_api_cart_item_schema', array( $this, 'extend_cart_item_schema' ) ); } /** * Extend cart schema for Store API. * * @param array $schema Schema. * @return array Modified schema. */ public function extend_cart_schema( $schema ) { $schema['dp_aicoso_pricing_applied'] = array( 'description' => 'Dynamic pricing applied', 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ); return $schema; } /** * Extend cart item schema for Store API. * * @param array $schema Schema. * @return array Modified schema. */ public function extend_cart_item_schema( $schema ) { $schema['dp_aicoso_original_price'] = array( 'description' => 'Original price before dynamic pricing', 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ); return $schema; } /** * Pre-process REST requests to ensure pricing is applied. * * @param mixed $response Response. * @param WP_REST_Server $server Server. * @param WP_REST_Request $request Request. * @return mixed Response. */ public function rest_pre_dispatch( $response, $server, $request ) { // Don't interfere - let WooCommerce handle REST requests naturally. // Our pricing is already applied via woocommerce_before_calculate_totals. return $response; } /** * Modify REST cart item response. * * @param WP_REST_Response $response Response object. * @param array $cart_item Cart item. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Modified response. */ public function modify_rest_cart_item( $response, $cart_item, $request ) { $data = $response->get_data(); // Ensure dynamic pricing is reflected in REST response. if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Calculate dynamic price (rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { // Update price in response. $data['price'] = wc_price( $dynamic_price ); $data['line_price'] = wc_price( $dynamic_price * $quantity ); // Add original price for reference. $data['dp_original_price'] = wc_price( $product->get_regular_price() ); } } $response->set_data( $data ); return $response; } /** * Modify Store API cart item price. * * @param string $price_html Price HTML. * @param array $cart_item Cart item. * @param string $cart_item_key Cart item key. * @return string Modified price. */ public function store_api_cart_item_price( $price_html, $cart_item, $cart_item_key ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Dynamic pricing rules will check their own disable settings. // Get the dynamic price. $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { return wc_price( $dynamic_price ); } } return $price_html; } /** * Modify Store API cart item prices array. * * @param array $prices Prices array. * @param array $cart_item Cart item. * @param string $cart_item_key Cart item key. * @return array Modified prices. */ public function store_api_cart_item_prices( $prices, $cart_item, $cart_item_key ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Get the dynamic price (rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price ) { // Convert to cents for Store API. $price_in_cents = round( $dynamic_price * 100 ); $prices['price'] = (string) $price_in_cents; $prices['regular_price'] = (string) round( $product->get_regular_price() * 100 ); $prices['sale_price'] = (string) $price_in_cents; // Line totals. $prices['line_total'] = (string) ( $price_in_cents * $quantity ); $prices['line_subtotal'] = (string) ( $price_in_cents * $quantity ); } } return $prices; } /** * AJAX handler for getting product price based on quantity. */ public function ajax_get_product_price() { // Log for debugging. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled error_log( 'DP_AICOSO AJAX: ajax_get_product_price called' ); } // Verify nonce. check_ajax_referer( 'dp_aicoso_public', 'nonce' ); $product_id = isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0; $quantity = isset( $_POST['quantity'] ) ? intval( $_POST['quantity'] ) : 1; if ( ! $product_id ) { wp_send_json_error( array( 'message' => 'Invalid product ID', 'product_id' => $product_id, ) ); return; } $product = wc_get_product( $product_id ); if ( ! $product ) { wp_send_json_error( array( 'message' => 'Product not found', 'product_id' => $product_id, ) ); return; } // Calculate dynamic price for the specified quantity (rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled error_log( 'DP_AICOSO: Calculated price: ' . $dynamic_price . ' for quantity: ' . $quantity ); } if ( false === $dynamic_price ) { $dynamic_price = $product->get_price(); } // Get original price for comparison. $regular_price = $product->get_regular_price(); // Build price HTML. $price_html = ''; if ( $dynamic_price < $regular_price ) { // Show sale price format. $price_html = wc_format_sale_price( $regular_price, $dynamic_price ); // Calculate savings. $savings = $regular_price - $dynamic_price; $savings_html = sprintf( '%s %s', __( 'You save:', 'aicoso-dynamic-pricing-for-woocommerce' ), wc_price( $savings ) ); } else { $price_html = wc_price( $dynamic_price ); $savings_html = ''; } // Add per-item price indicator for quantity > 1. if ( $quantity > 1 ) { $price_html .= sprintf( ' (%s %s)', wc_price( $dynamic_price ), __( 'each', 'aicoso-dynamic-pricing-for-woocommerce' ), ); } wp_send_json_success( array( 'price' => $dynamic_price, 'price_html' => $price_html, 'regular_price' => $regular_price, 'savings' => isset( $savings_html ) ? $savings_html : '', 'quantity' => $quantity, 'debug' => array( 'product_id' => $product_id, 'calculated_price' => $dynamic_price, 'regular_price' => $regular_price, ), ) ); } /** * Shortcode to display dynamic price. * * @param array $atts Shortcode attributes. * @return string Price HTML. */ public function dynamic_price_shortcode( $atts ) { global $product; $atts = shortcode_atts( array( 'product_id' => 0, 'quantity' => 1, ), $atts ); // Get product. $current_product = null; if ( $atts['product_id'] ) { $current_product = wc_get_product( $atts['product_id'] ); } elseif ( $product ) { $current_product = $product; } if ( ! $current_product ) { return ''; } // Calculate price. $price = $this->calculator->calculate_product_price( $current_product, $atts['quantity'] ); if ( false === $price ) { $price = $current_product->get_price(); } return wc_price( $price ); } /** * Set quantity input args to show cart quantity as default. * * @param array $args Quantity input args. * @param WC_Product $product Product object. * @return array Modified args. */ public function set_quantity_input_args( $args, $product ) { // Only on single product pages. if ( ! is_product() ) { return $args; } // Skip for variable products. if ( $product && $product->is_type( 'variable' ) ) { return $args; } // Only for main product, not related products. global $woocommerce_loop, $post; if ( ! empty( $woocommerce_loop['name'] ) ) { return $args; } // Check if this is the main product. if ( ! $post || ! $product || $product->get_id() != $post->ID ) { return $args; } // Get cart quantity for this product. $cart_quantity = $this->get_product_cart_quantity( $product ); // If product is in cart, set that as the input value. if ( $cart_quantity > 0 ) { $args['input_value'] = $cart_quantity; } return $args; } /** * Refresh mini cart prices when cart is loaded from session. * * @param WC_Cart $cart Cart object. */ public function refresh_mini_cart_prices( $cart ) { if ( ! $cart || $cart->is_empty() ) { return; } // Force recalculation for mini cart display. foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Calculate dynamic price (rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price && is_numeric( $dynamic_price ) ) { $product->set_price( $dynamic_price ); } } } } /** * Fix mini cart item price display. * * @param string $quantity_html Quantity HTML. * @param array $cart_item Cart item. * @param string $cart_item_key Cart item key. * @return string Modified HTML. */ public function mini_cart_item_price( $quantity_html, $cart_item, $cart_item_key ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] ) { $product = $cart_item['data']; $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Get the dynamic price (rules will check their own disable settings). $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); if ( false !== $dynamic_price && is_numeric( $dynamic_price ) ) { // Format the quantity string with correct price. $quantity_html = '' . sprintf( '%s × %s', $quantity, wc_price( $dynamic_price ) ) . ''; } } return $quantity_html; } /** * Modify variation data to include dynamic pricing. * * @param array $data Variation data. * @param WC_Product $product Parent variable product. * @param WC_Product_Variation $variation Variation product. * @return array Modified data. */ public function modify_variation_data( $data, $product, $variation ) { if ( ! $variation ) { return $data; } $regular_price = $variation->get_regular_price(); if ( empty( $regular_price ) ) { return $data; } // Calculate dynamic price for this variation. $dynamic_price = $this->calculator->calculate_product_price( $variation, 1 ); if ( false !== $dynamic_price && is_numeric( $dynamic_price ) && $dynamic_price > 0 && $dynamic_price < $regular_price ) { // Update the price HTML to show discount. $data['price_html'] = wc_format_sale_price( $regular_price, $dynamic_price ); // Also update the display price for JavaScript. $data['display_price'] = $dynamic_price; $data['display_regular_price'] = $regular_price; } return $data; } /** * Check if product is being displayed in sticky navigation/header. * * @param WC_Product $product Product to check. * @return bool True if in sticky nav. */ private function is_in_sticky_nav( $product ) { // This is a simplified check - themes may vary. // Most sticky navs don't use the woocommerce_loop. // but they do show the same product as the main page. // You can extend this based on your theme's structure. // For now, we'll assume any product matching the main post ID. // that's not explicitly in a related products loop is the main product. return false; // Conservative approach for now. } /** * Get the quantity of a product that's already in the cart. * * @param WC_Product $product Product to check. * @return int Quantity in cart (0 if not in cart). */ private function get_product_cart_quantity( $product ) { if ( ! WC()->cart || WC()->cart->is_empty() ) { return 0; } $product_id = $product->get_id(); $parent_id = $product->get_parent_id(); $total_quantity = 0; foreach ( WC()->cart->get_cart() as $cart_item ) { // Check if this cart item matches our product. $matches = false; if ( isset( $cart_item['product_id'] ) ) { // Check direct product ID match. if ( $cart_item['product_id'] == $product_id ) { $matches = true; } // Check parent ID match for variations. if ( $parent_id && $cart_item['product_id'] == $parent_id ) { // Also check variation ID if it's a variation. if ( isset( $cart_item['variation_id'] ) && $cart_item['variation_id'] == $product_id ) { $matches = true; } } } if ( $matches ) { $total_quantity += isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 0; } } return $total_quantity; } /** * Handle coupon change events. * Triggers cart recalculation when coupons are added or removed. * * @param string $coupon_code The coupon code that was added/removed. */ public function handle_coupon_change( $coupon_code = '' ) { if ( ! WC()->cart ) { return; } // Clear any cached prices. $calculator = DP_AICOSO_Price_Calculator::get_instance(); if ( method_exists( $calculator, 'clear_cache' ) ) { $calculator->clear_cache(); } // Trigger cart recalculation to update prices. WC()->cart->calculate_totals(); } }