raverses and modifies the meta_query array to adjust '_price' values * from the 'from_currency' to the 'target_currency'. * * @param array $meta_query The meta_query array to traverse. * @param string $from_currency The from currency code. * @param string $target_currency The target currency code. * @param int $depth The current depth of the recursion. * * @return array The modified meta_query array. */ private function convert_meta_query_price_filters( $meta_query, $from_currency, $target_currency, $depth = 0 ) { // Prevent infinite recursion in a malformed meta_query. if ( $depth > 4 ) { return $meta_query; } foreach ( $meta_query as &$mq ) { // If the current element is a nested meta_query with a relation. if ( isset( $mq['relation'] ) && is_array( $mq ) ) { // Recursively modify the nested meta_query. if ( isset( $mq['relation'] ) ) { // Extract the relation and the nested queries. $relation = $mq['relation']; $modified_nested = $this->convert_meta_query_price_filters( $mq, $from_currency, $target_currency, $depth + 1 ); // Reconstruct the meta_query with the modified nested queries. $mq = array_merge( [ 'relation' => $relation ], $modified_nested ); } } elseif ( isset( $mq['key'] ) && '_price' === $mq['key'] && isset( $mq['value'] ) && is_numeric( $mq['value'] ) ) { $converted_price = $this->multi_currency->get_raw_conversion( $mq['value'], $from_currency, $target_currency ); if ( is_numeric( $converted_price ) ) { // Apply floor or ceil based on the 'compare' operator. if ( isset( $mq['compare'] ) ) { if ( '<=' === $mq['compare'] ) { $mq['value'] = (string) ceil( $converted_price ); // max_price. } elseif ( '>=' === $mq['compare'] ) { $mq['value'] = (string) floor( $converted_price ); // min_price. } } } } } unset( $mq ); return $meta_query; } /** * Modify the products/collection-data REST API response to include converted price ranges. * * @param \WP_REST_Response $response The original REST response. * @param \WP_REST_Server $server The REST server instance. * @param \WP_REST_Request $request The REST request instance. * * @return \WP_REST_Response The modified REST response. */ public function maybe_modify_price_ranges_rest_response( $response, $server, $request ) { if ( '/wc/store/v1/products/collection-data' !== $request->get_route() ) { return $response; } $data = $response->get_data(); if ( empty( $data['price_range'] ) || ! is_object( $data['price_range'] ) ) { return $response; } $store_currency = $this->multi_currency->get_default_currency()->get_code(); $selected_currency = $this->multi_currency->get_selected_currency()->get_code(); if ( $store_currency === $selected_currency ) { return $response; } $price_fields = [ 'min_price', 'max_price' ]; foreach ( $price_fields as $field ) { if ( property_exists( $data['price_range'], $field ) && is_numeric( $data['price_range']->$field ) ) { $converted_price = $this->multi_currency->get_price( $data['price_range']->$field, 'product' ); if ( is_numeric( $converted_price ) ) { $data['price_range']->$field = (string) $converted_price; } } } $response->set_data( $data ); return $response; } /** * Returns the price for a product. * * @param mixed $price The product's price. * @param mixed $product WC_Product or null. * * @return mixed The converted product's price. */ public function get_product_price( $price, $product = null ) { if ( ! $price || ! $this->compatibility->should_convert_product_price( $product ) ) { return $price; } return $this->multi_currency->get_price( $price, 'product' ); } /** * Returns the stringified price for a product. * * @param mixed $price The product's price. * @param mixed $product WC_Product or null. * * @return string The converted product's price. */ public function get_product_price_string( $price, $product = null ): string { return (string) $this->get_product_price( $price, $product ); } /** * Returns the price range for a variation. * * @param array $variation_prices The variation's prices. * * @return array The converted variation's prices. */ public function get_variation_price_range( $variation_prices ) { foreach ( $variation_prices as $price_type => $prices ) { foreach ( $prices as $variation_id => $price ) { $variation_prices[ $price_type ][ $variation_id ] = $this->get_product_price_string( $price ); } } return $variation_prices; } /** * Add the exchange rate into account for the variation prices hash. * This is used to recalculate the variation price range when the exchange * rate changes, otherwise the old prices will be cached. * * @param array $prices_hash The variation prices hash. * * @return array The variation prices hash with the current exchange rate. */ public function add_exchange_rate_to_variation_prices_hash( $prices_hash ) { $prices_hash[] = $this->get_product_price( 1 ); return $prices_hash; } /** * Returns the shipping add rate args with cost converted. * * @param array $args Shipping rate args. * * @return array Shipping rate args with converted cost. */ public function convert_shipping_method_rate_cost( $args ) { $cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost']; $args = wp_parse_args( [ 'cost' => $this->multi_currency->get_price( $cost, 'shipping' ), ], $args ); return $args; } /** * Returns the amount for a coupon. * * @param mixed $amount The coupon's amount. * @param object $coupon The coupon object. * * @return mixed The converted coupon's amount. */ public function get_coupon_amount( $amount, $coupon ) { $percent_coupon_types = [ 'percent' ]; if ( ! $amount || $coupon->is_type( $percent_coupon_types ) || ! $this->compatibility->should_convert_coupon_amount( $coupon ) ) { return $amount; } return $this->multi_currency->get_price( $amount, 'coupon' ); } /** * Returns the min or max amount for a coupon. * * @param mixed $amount The coupon's min or max amount. * * @return mixed The converted coupon's min or max amount. */ public function get_coupon_min_max_amount( $amount ) { if ( ! $amount ) { return $amount; } // Coupon mix/max prices are treated as products to avoid inconsistencies with charm pricing // making a coupon invalid when the coupon min/max amount is the same as the product's price. return $this->multi_currency->get_price( $amount, 'product' ); } /** * Returns the free shipping zone settings with converted min_amount. * * @param array $data The shipping zone settings. * * @return array The shipping zone settings with converted min_amount. */ public function get_free_shipping_min_amount( $data ) { if ( empty( $data['min_amount'] ) ) { return $data; } // Free shipping min amount is treated as products to avoid inconsistencies with charm pricing // making a method invalid when its min amount is the same as the product's price. $data['min_amount'] = $this->multi_currency->get_price( $data['min_amount'], 'product' ); return $data; } /** * Register the hooks to set the min amount for free shipping methods. */ public function register_free_shipping_filters() { $shipping_zones = \WC_Shipping_Zones::get_zones(); $default_zone = \WC_Shipping_Zones::get_zone( 0 ); if ( $default_zone ) { $shipping_zones[] = [ 'shipping_methods' => $default_zone->get_shipping_methods() ]; } foreach ( $shipping_zones as $shipping_zone ) { foreach ( $shipping_zone['shipping_methods'] as $shipping_method ) { if ( 'free_shipping' === $shipping_method->id ) { $option_name = 'option_woocommerce_' . trim( $shipping_method->id ) . '_' . (int) $shipping_method->instance_id . '_settings'; add_filter( $option_name, [ $this, 'get_free_shipping_min_amount' ], 99 ); } } } } /** * Adds the exchange rate and default currency to the order's meta if prices have been converted. * * @param int $order_id The order ID. * @param WC_Order $order The order object. */ public function add_order_meta( $order_id, $order ) { $default_currency = $this->multi_currency->get_default_currency(); // Do not add exchange rate if order was made in the store's default currency. if ( $default_currency->get_code() === $order->get_currency() ) { return; } $exchange_rate = $this->multi_currency->get_price( 1, 'exchange_rate' ); $order->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $exchange_rate ); $order->update_meta_data( '_wcpay_multi_currency_order_default_currency', $default_currency->get_code() ); $order->save_meta_data(); } }