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();
}
}