Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update currency conversion method for booking products #10042

Merged
merged 5 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/fix-472-mccy-wc-bookings
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Update currency conversion method for booking products.
62 changes: 43 additions & 19 deletions includes/multi-currency/Compatibility/WooCommerceBookings.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use WCPay\MultiCurrency\FrontendCurrencies;
use WCPay\MultiCurrency\MultiCurrency;
use WCPay\MultiCurrency\Utils;
use WC_Product;

/**
* Class that controls Multi Currency Compatibility with WooCommerce Bookings Plugin.
Expand Down Expand Up @@ -43,22 +44,44 @@ public function init() {
// Add needed actions and filters if Bookings is active.
if ( class_exists( 'WC_Bookings' ) ) {
if ( ! is_admin() || wp_doing_ajax() ) {
add_filter( 'woocommerce_bookings_calculated_booking_cost', [ $this, 'adjust_amount_for_calculated_booking_cost' ], 50, 1 );
add_filter( 'woocommerce_product_get_block_cost', [ $this, 'get_price' ], 50, 1 );
add_filter( 'woocommerce_product_get_cost', [ $this, 'get_price' ], 50, 1 );
add_filter( 'woocommerce_product_get_display_cost', [ $this, 'get_price' ], 50, 1 );
add_filter( 'woocommerce_product_booking_person_type_get_block_cost', [ $this, 'get_price' ], 50, 1 );
add_filter( 'woocommerce_product_booking_person_type_get_cost', [ $this, 'get_price' ], 50, 1 );
add_filter( 'woocommerce_product_get_resource_base_costs', [ $this, 'get_resource_prices' ], 50, 1 );
add_filter( 'woocommerce_product_get_resource_block_costs', [ $this, 'get_resource_prices' ], 50, 1 );
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ] );
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
add_action( 'wp_ajax_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 );
add_action( 'wp_ajax_nopriv_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 );
}
}
}

/**
* Returns the price for an item.
* Adjusts the calculated booking cost for the selected currency, applying rounding and charm pricing as necessary.
*
* @param mixed $costs The original calculated booking costs.
* @return mixed The booking cost adjusted for the selected currency.
*/
public function adjust_amount_for_calculated_booking_cost( $costs ) {
/**
* Prevents adjustment of the calculated booking cost during cart addition.
*
* When a booking is added to the cart, the Booking plugin calculates the booking cost and
* overrides the cart item price with this calculated amount. To avoid interfering with this process,
* this function skips any additional adjustments at this stage.
*/
if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) ) {
return $costs;
}

return $this->multi_currency->adjust_amount_for_selected_currency( $costs );
}

/**
* Retrieves the price for an item, converting it based on the selected currency and context.
*
* @param mixed $price The item's price.
*
Expand All @@ -68,7 +91,19 @@ public function get_price( $price ) {
if ( ! $price ) {
return $price;
}
return $this->multi_currency->get_price( $price, 'product' );

// Skip conversion during specific booking cost calculations to avoid double conversion.
if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Bookings_Cost_Calculation::calculate_booking_cost' ] ) ) {
return $price;
}

/**
* When showing the price in HTML, the function applies currency conversion, charm pricing,
* and rounding. For internal calculations, it uses the raw exchange rate, with charm pricing
* and rounding adjustments applied only to the final calculated amount (handled in
* adjust_amount_for_calculated_booking_cost).
*/
return $this->multi_currency->get_price( $price, $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ? 'product' : 'exchange_rate' );
}

/**
Expand All @@ -90,28 +125,17 @@ public function get_resource_prices( $prices ) {
/**
* Checks to see if the product's price should be converted.
*
* @param bool $return Whether to convert the product's price or not. Default is true.
* @param bool $return Whether to convert the product's price or not. Default is true.
* @param WC_Product $product The product instance being checked.
*
* @return bool True if it should be converted.
*/
public function should_convert_product_price( bool $return ): bool {
// If it's already false, return it.
if ( ! $return ) {
public function should_convert_product_price( bool $return, WC_Product $product ): bool {
// If it's already false, or the product is not a booking, ignore it.
if ( ! $return || $product->get_type() !== 'booking' ) {
return $return;
}

// This prevents a double conversion of the price in the cart.
if ( $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price' ] ) ) {
$calls = [
'WC_Cart_Totals->calculate_item_totals',
'WC_Cart->get_product_price',
'WC_Cart->get_product_subtotal',
];
if ( $this->utils->is_call_in_backtrace( $calls ) ) {
return false;
}
}

// Fixes price display on product page and in shop.
if ( $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ) {
return false;
Expand Down
26 changes: 20 additions & 6 deletions includes/multi-currency/MultiCurrency.php
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ public function set_client_format_and_rounding_precision() {
woocommerce_admin_meta_boxes.rounding_precision = <?php echo (int) $rounding_precision; ?>;
</script>
<?php
endif;
endif;
}

/**
Expand Down Expand Up @@ -1252,10 +1252,10 @@ public function get_multi_currency_onboarding_simulation_variables() {
public function is_multi_currency_settings_page(): bool {
global $current_screen, $current_tab;
return (
is_admin()
&& $current_tab && $current_screen
&& 'wcpay_multi_currency' === $current_tab
&& 'woocommerce_page_wc-settings' === $current_screen->base
is_admin()
&& $current_tab && $current_screen
&& 'wcpay_multi_currency' === $current_tab
&& 'woocommerce_page_wc-settings' === $current_screen->base
);
}

Expand All @@ -1277,7 +1277,7 @@ public function get_all_customer_currencies(): array {
$query_union = [];

if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) &&
\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
foreach ( $currencies as $currency ) {
$query_union[] = $wpdb->prepare(
"SELECT %s AS currency_code, EXISTS(SELECT currency FROM {$wpdb->prefix}wc_orders WHERE currency=%s LIMIT 1) AS exists_in_orders",
Expand Down Expand Up @@ -1327,6 +1327,20 @@ public function is_initialized(): bool {
return static::$is_initialized;
}

/**
* Adjusts the given amount for the currently selected currency.
*
* Applies charm pricing if specified, and adjusts the amount according to
* the selected currency's conversion rate.
*
* @param float $amount The original amount to adjust.
* @param bool $apply_charm_pricing Optional. Whether to apply charm pricing to the adjusted amount. Default true.
* @return float The amount adjusted for the selected currency.
*/
public function adjust_amount_for_selected_currency( $amount, $apply_charm_pricing = true ) {
return $this->get_adjusted_price( $amount, $apply_charm_pricing, $this->get_selected_currency() );
}

/**
* Returns the amount with the backend format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class WCPay_Multi_Currency_WooCommerceBookings_Tests extends WCPAY_UnitTestCase
*/
private $localization_service;

/**
* Mock product.
*
* @var \WC_Product|PHPUnit_Framework_MockObject_MockObject
*/
private $mock_product;

/**
* Pre-test setup
*/
Expand All @@ -62,6 +69,14 @@ public function set_up() {
$this->mock_frontend_currencies = $this->createMock( FrontendCurrencies::class );
$this->woocommerce_bookings = new WooCommerceBookings( $this->mock_multi_currency, $this->mock_utils, $this->mock_frontend_currencies );
$this->localization_service = new WC_Payments_Localization_Service();

$this->mock_product = $this->createMock( \WC_Product::class );
$this->mock_product
->method( 'get_id' )
->willReturn( 42 );
$this->mock_product
->method( 'get_type' )
->willReturn( 'booking' );
}

public function test_get_price_returns_empty_string() {
Expand Down Expand Up @@ -95,53 +110,29 @@ public function test_get_resource_prices_returns_converted_prices() {
// If false is passed, it should automatically return false.
public function test_should_convert_product_price_returns_false_if_false_passed() {
$this->mock_utils->expects( $this->exactly( 0 ) )->method( 'is_call_in_backtrace' );
$this->assertFalse( $this->woocommerce_bookings->should_convert_product_price( false ) );
$this->assertFalse( $this->woocommerce_bookings->should_convert_product_price( false, $this->mock_product ) );
}

// If the first two sets of calls are found, it should return false.
public function test_should_convert_product_price_returns_false_if_cart_calls_found() {
$first_calls = [ 'WC_Product_Booking->get_price' ];
$second_calls = [
'WC_Cart_Totals->calculate_item_totals',
'WC_Cart->get_product_price',
'WC_Cart->get_product_subtotal',
];
$this->mock_utils
->expects( $this->exactly( 2 ) )
->method( 'is_call_in_backtrace' )
->withConsecutive( [ $first_calls ], [ $second_calls ] )
->willReturn( true, true );
$this->assertFalse( $this->woocommerce_bookings->should_convert_product_price( true ) );
}

// If the last set of calls are found, it should return false.
// This also tests to make sure if the first set of calls is found, but not the second, it continues.
// If the get_price_html call is found, it should return false.
public function test_should_convert_product_price_returns_false_if_display_calls_found() {
$first_calls = [ 'WC_Product_Booking->get_price' ];
$second_calls = [
'WC_Cart_Totals->calculate_item_totals',
'WC_Cart->get_product_price',
'WC_Cart->get_product_subtotal',
];
$third_calls = [ 'WC_Product_Booking->get_price_html' ];
$expected_calls = [ 'WC_Product_Booking->get_price_html' ];
$this->mock_utils
->expects( $this->exactly( 3 ) )
->expects( $this->exactly( 1 ) )
->method( 'is_call_in_backtrace' )
->withConsecutive( [ $first_calls ], [ $second_calls ], [ $third_calls ] )
->willReturn( true, false, true );
$this->assertFalse( $this->woocommerce_bookings->should_convert_product_price( true ) );
->with( $expected_calls )
->willReturn( true );
$this->assertFalse( $this->woocommerce_bookings->should_convert_product_price( true, $this->mock_product ) );
}

// If no calls are found, it should return true.
public function test_should_convert_product_price_returns_true_if_no_calls_found() {
$first_calls = [ 'WC_Product_Booking->get_price' ];
$third_calls = [ 'WC_Product_Booking->get_price_html' ];
$expected_calls = [ 'WC_Product_Booking->get_price_html' ];
$this->mock_utils
->expects( $this->exactly( 2 ) )
->expects( $this->exactly( 1 ) )
->method( 'is_call_in_backtrace' )
->withConsecutive( [ $first_calls ], [ $third_calls ] )
->willReturn( false, false );
$this->assertTrue( $this->woocommerce_bookings->should_convert_product_price( true ) );
->with( $expected_calls )
->willReturn( false );
$this->assertTrue( $this->woocommerce_bookings->should_convert_product_price( true, $this->mock_product ) );
}

public function test_filter_wc_price_args_returns_expected_results() {
Expand Down
Loading