Files
shuffle_and_skirmish_website/wp-content/plugins/woocommerce-payments/includes/multi-currency/FrontendPrices.php
2025-11-24 21:33:55 +00:00

376 lines
12 KiB
PHP

<?php
/**
* WooCommerce Payments Multi-Currency Frontend Prices
*
* @package WooCommerce\Payments
*/
namespace WCPay\MultiCurrency;
use WC_Order;
defined( 'ABSPATH' ) || exit;
/**
* Class that applies Multi-Currency prices on the frontend.
*/
class FrontendPrices {
/**
* Compatibility instance.
*
* @var Compatibility
*/
protected $compatibility;
/**
* Multi-Currency instance.
*
* @var MultiCurrency
*/
protected $multi_currency;
/**
* Constructor.
*
* @param MultiCurrency $multi_currency The MultiCurrency instance.
* @param Compatibility $compatibility The Compatibility instance.
*/
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
$this->multi_currency = $multi_currency;
$this->compatibility = $compatibility;
}
/**
* Initializes this class' WP hooks.
*
* @return void
*/
public function init_hooks() {
if ( defined( 'DOING_CRON' ) || is_admin() || Utils::is_admin_api_request() ) {
return;
}
// Simple product price hooks.
add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price_string' ], 99, 2 );
add_filter( 'woocommerce_product_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 );
add_filter( 'woocommerce_product_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 );
// Variation price hooks.
add_filter( 'woocommerce_product_variation_get_price', [ $this, 'get_product_price_string' ], 99, 2 );
add_filter( 'woocommerce_product_variation_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 );
add_filter( 'woocommerce_product_variation_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 );
// Variation price range hooks.
add_filter( 'woocommerce_variation_prices', [ $this, 'get_variation_price_range' ], 99 );
add_filter( 'woocommerce_get_variation_prices_hash', [ $this, 'add_exchange_rate_to_variation_prices_hash' ], 99 );
// Shipping methods hooks.
add_filter( 'woocommerce_shipping_zone_shipping_methods', [ $this, 'convert_free_shipping_method_min_amount' ], 99 );
add_filter( 'woocommerce_shipping_method_add_rate_args', [ $this, 'convert_shipping_method_rate_cost' ], 99 );
// Coupon hooks.
add_filter( 'woocommerce_coupon_get_amount', [ $this, 'get_coupon_amount' ], 99, 2 );
add_filter( 'woocommerce_coupon_get_minimum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 );
add_filter( 'woocommerce_coupon_get_maximum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 );
// Order hooks.
add_filter( 'woocommerce_new_order', [ $this, 'add_order_meta' ], 99, 2 );
// Price Filter Hooks.
add_filter( 'rest_post_dispatch', [ $this, 'maybe_modify_price_ranges_rest_response' ], 10, 3 );
add_filter( 'query_loop_block_query_vars', [ $this, 'maybe_modify_price_ranges_query_var' ], 10, 3 );
}
/**
* Modifies the price range query parameters when the selected currency is not the same as the store currency.
*
* This method converts the '_price' parameters based on the selected currency.
*
* @param array $query The current query variables.
* @param \WP_Block $block The current block instance.
* @param int $page The current page number.
*
* @return array The modified query variables.
*/
public function maybe_modify_price_ranges_query_var( $query, $block, $page ) {
if ( 'product' !== $query['post_type'] ) {
return $query;
}
if ( empty( $query['meta_query'] ) || ! is_array( $query['meta_query'] ) ) {
return $query;
}
$store_currency = $this->multi_currency->get_default_currency()->get_code();
$selected_currency = $this->multi_currency->get_selected_currency()->get_code();
// If currencies are the same, no need to convert prices in the query.
if ( $store_currency === $selected_currency ) {
return $query;
}
// Traverse and modify the meta_query array.
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$query['meta_query'] = $this->convert_meta_query_price_filters( $query['meta_query'], $store_currency, $selected_currency );
return $query;
}
/**
* Recursively traverses 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 ) {
if ( isset( $args['cost'] ) ) {
/**
* We need to keep the `cost` structure intact when applying
* multi-currency conversions, because downstream it is important
* for WooCommerce to keep the taxes flow consistent.
*/
if ( is_array( $args['cost'] ) ) {
$args['cost'] = array_map(
function ( $cost ) {
return $this->multi_currency->get_price( $cost, 'shipping' );
},
$args['cost']
);
} else {
$args['cost'] = $this->multi_currency->get_price( $args['cost'], 'shipping' );
}
}
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' );
}
/**
* Converts the min_amount of free shipping methods.
*
* @param array $methods The shipping methods.
*/
public function convert_free_shipping_method_min_amount( $methods ) {
foreach ( $methods as $method ) {
// 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.
if ( 'free_shipping' === $method->id && ! empty( $method->min_amount ) ) {
$method->min_amount = $this->multi_currency->get_price( $method->min_amount, 'product' );
}
}
return $methods;
}
/**
* 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();
}
}