Skip to content

Commit

Permalink
Update currency conversion method for booking products (#10042)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelzaleski authored Dec 27, 2024
1 parent f2c2d52 commit 442ba90
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 61 deletions.
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

0 comments on commit 442ba90

Please sign in to comment.