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

Respect BNPL limits_per_currency (country, min, max) #9626

Merged
merged 10 commits into from
Oct 31, 2024
4 changes: 4 additions & 0 deletions changelog/fix-9456-bnpl-respect-limits-per-currency
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Prevent dead space on product pages when no BNPL offers are available.
5 changes: 2 additions & 3 deletions client/product-details/bnpl-site-messaging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ export const initializeBnplSiteMessaging = async () => {
isCart,
isCartBlock,
cartTotal,
minimumOrderAmount,
isBnplAvailable,
} = window.wcpayStripeSiteMessaging;

let amount;
let elementLocation = 'bnplProductPage';
const minOrderAmount = parseInt( minimumOrderAmount, 10 ) || 0;
const paymentMessageContainer = document.getElementById(
'payment-method-message'
);
Expand All @@ -71,7 +70,7 @@ export const initializeBnplSiteMessaging = async () => {
} else {
amount = parseInt( productVariations.base_product.amount, 10 ) || 0;

if ( amount < minOrderAmount ) {
if ( ! isBnplAvailable ) {
paymentMessageContainer.style.setProperty( 'display', 'none' );
}
}
Expand Down
36 changes: 34 additions & 2 deletions client/product-details/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,29 @@ jQuery( async function ( $ ) {
'get_cart_total'
),
{
security: window.wcpayStripeSiteMessaging.nonce,
security: window.wcpayStripeSiteMessaging.nonce.get_cart_total,
}
);
};

const isBnplAvailable = ( price, currency, country ) => {
return request(
buildAjaxURL(
window.wcpayStripeSiteMessaging.wcAjaxUrl,
'check_bnpl_availability'
),
{
security:
window.wcpayStripeSiteMessaging.nonce.is_bnpl_available,
price: price,
currency: currency,
country: country,
}
);
};

// Update BNPL message based on the quantity change
quantityInput.on( 'change', ( event ) => {
quantityInput.on( 'change', async ( event ) => {
let amount = baseProductAmount;
const variationId = $( VARIATION_ID_SELECTOR ).val();

Expand All @@ -123,6 +139,22 @@ jQuery( async function ( $ ) {
}

updateBnplPaymentMessage( amount, productCurrency, event.target.value );

// Check if changes in quantity/price affect BNPL availability and show/hide BNPL messaging accordingly.
try {
const response = await isBnplAvailable(
amount * event.target.value,
productCurrency,
window.wcpayStripeSiteMessaging.country
);
if ( response.success && response.data.is_available ) {
$( '#payment-method-message' ).slideDown();
} else {
$( '#payment-method-message' ).slideUp();
}
} catch {
// Do nothing.
}
} );

$( document.body ).on( 'updated_cart_totals', () => {
Expand Down
49 changes: 32 additions & 17 deletions includes/class-wc-payments-payment-method-messaging-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public function init() {
'currency' => $currency_code,
],
];

$product_price = $product_variations['base_product']['amount'];

foreach ( $product->get_children() as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( $variation ) {
Expand All @@ -98,11 +101,13 @@ public function init() {
'amount' => WC_Payments_Utils::prepare_amount( $price, $currency_code ),
'currency' => $currency_code,
];

$product_price = $product_variations['base_product']['amount'];
}
}
}

$enabled_upe_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout();
$enabled_upe_payment_methods = $this->gateway->get_upe_enabled_payment_method_ids();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just ensures we're getting all of the enabled BNPL payment methods. get_payment_method_ids_enabled_at_checkout() is a filtered list for checkout. This means, if you visit a PDP while you have products in the cart, you might not see the correct BNPL methods in the PMME. For example, with a $5 product in the cart, a $60 PDP won't display Affirm ($50 minimum) because it's been filtered out based on cart totals. Demo here, second example from the bottom. This change solves that.

// Filter non BNPL out of the list of payment methods.
$bnpl_payment_methods = array_intersect( $enabled_upe_payment_methods, Payment_Method::BNPL_PAYMENT_METHODS );

Expand All @@ -118,26 +123,36 @@ public function init() {
WC_Payments::get_file_version( 'dist/product-details.css' ),
);

$country = empty( $billing_country ) ? $store_country : $billing_country;

$script_data = [
'productId' => 'base_product',
'productVariations' => $product_variations,
'country' => $country,
'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ),
'accountId' => $this->account->get_stripe_account_id(),
'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ),
'paymentMethods' => array_values( $bnpl_payment_methods ),
'currencyCode' => $currency_code,
'isCart' => is_cart(),
'isCartBlock' => $is_cart_block,
'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ),
'nonce' => [
'get_cart_total' => wp_create_nonce( 'wcpay-get-cart-total' ),
'is_bnpl_available' => wp_create_nonce( 'wcpay-is-bnpl-available' ),
],
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
];

if ( $product ) {
$script_data['isBnplAvailable'] = WC_Payments_Utils::is_any_bnpl_method_available( array_values( $bnpl_payment_methods ), $country, $currency_code, $product_price );
}

// Create script tag with config.
wp_localize_script(
'WCPAY_PRODUCT_DETAILS',
'wcpayStripeSiteMessaging',
[
'productId' => 'base_product',
'productVariations' => $product_variations,
'country' => empty( $billing_country ) ? $store_country : $billing_country,
'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ),
'accountId' => $this->account->get_stripe_account_id(),
'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ),
'paymentMethods' => array_values( $bnpl_payment_methods ),
'currencyCode' => $currency_code,
'isCart' => is_cart(),
'isCartBlock' => $is_cart_block,
'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ),
'minimumOrderAmount' => WC_Payments_Utils::get_cached_minimum_amount( $currency_code, true ),
'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ),
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
]
$script_data
);

// Ensure wcpayConfig is available in the page.
Expand Down
210 changes: 153 additions & 57 deletions includes/class-wc-payments-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -716,59 +716,163 @@ public static function get_filtered_error_status_code( Exception $e ): int {
}

/**
* Retrieves Stripe minimum order value authorized per currency.
* The values are based on Stripe's recommendations.
* See https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts.
* Get the BNPL limits per currency for a specific payment method.
*
* @param string $currency The currency.
*
* @return int The minimum amount.
* @param string $payment_method The payment method name ('affirm', 'afterpay_clearpay', or 'klarna').
* @return array The BNPL limits per currency for the specified payment method.
*/
public static function get_stripe_minimum_amount( $currency ) {
switch ( $currency ) {
case 'AED':
case 'MYR':
case 'PLN':
case 'RON':
$minimum_amount = 200;
break;
case 'BGN':
$minimum_amount = 100;
break;
case 'CZK':
$minimum_amount = 1500;
break;
case 'DKK':
$minimum_amount = 250;
break;
case 'GBP':
$minimum_amount = 30;
break;
case 'HKD':
$minimum_amount = 400;
break;
case 'HUF':
$minimum_amount = 17500;
break;
case 'JPY':
$minimum_amount = 5000;
break;
case 'MXN':
case 'THB':
$minimum_amount = 1000;
break;
case 'NOK':
case 'SEK':
$minimum_amount = 300;
break;
Comment on lines -727 to -763
Copy link
Member Author

@mdmoore mdmoore Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned in the PR description, this was added to handle BNPL minimum amounts but is actually comprised of Stripe minimum amounts. It's was only used for that purpose and is safe to remove.

public static function get_bnpl_limits_per_currency( $payment_method ) {
switch ( $payment_method ) {
case 'affirm':
return [
Currency_Code::CANADIAN_DOLLAR => [
Country_Code::CANADA => [
'min' => 5000,
'max' => 3000000,
], // Represents CAD 50 - 30,000 CAD.
],
Currency_Code::UNITED_STATES_DOLLAR => [
Country_Code::UNITED_STATES => [
'min' => 5000,
'max' => 3000000,
],
], // Represents USD 50 - 30,000 USD.
];
case 'afterpay_clearpay':
return [
Currency_Code::AUSTRALIAN_DOLLAR => [
Country_Code::AUSTRALIA => [
'min' => 100,
'max' => 200000,
], // Represents AUD 1 - 2,000 AUD.
],
Currency_Code::CANADIAN_DOLLAR => [
Country_Code::CANADA => [
'min' => 100,
'max' => 200000,
], // Represents CAD 1 - 2,000 CAD.
],
Currency_Code::NEW_ZEALAND_DOLLAR => [
Country_Code::NEW_ZEALAND => [
'min' => 100,
'max' => 200000,
], // Represents NZD 1 - 2,000 NZD.
],
Currency_Code::POUND_STERLING => [
Country_Code::UNITED_KINGDOM => [
'min' => 100,
'max' => 120000,
], // Represents GBP 1 - 1,200 GBP.
],
Currency_Code::UNITED_STATES_DOLLAR => [
Country_Code::UNITED_STATES => [
'min' => 100,
'max' => 400000,
], // Represents USD 1 - 4,000 USD.
],
];
case 'klarna':
return [
Currency_Code::UNITED_STATES_DOLLAR => [
Country_Code::UNITED_STATES => [
'min' => 100,
'max' => 1000000,
], // Represents USD 1 - 10,000 USD.
],
Currency_Code::POUND_STERLING => [
Country_Code::UNITED_KINGDOM => [
'min' => 100,
'max' => 500000,
], // Represents GBP 1 - 5,000 GBP.
],
Currency_Code::EURO => [
Country_Code::AUSTRIA => [
'min' => 100,
'max' => 1000000,
], // Represents EUR 1 - 10,000 EUR.
Country_Code::BELGIUM => [
'min' => 100,
'max' => 1000000,
], // Represents EUR 1 - 10,000 EUR.
Country_Code::GERMANY => [
'min' => 100,
'max' => 1000000,
], // Represents EUR 1 - 10,000 EUR.
Country_Code::NETHERLANDS => [
'min' => 100,
'max' => 500000,
], // Represents EUR 1 - 5,000 EUR.
Country_Code::FINLAND => [
'min' => 100,
'max' => 1000000,
], // Represents EUR 1 - 10,000 EUR.
Country_Code::SPAIN => [
'min' => 100,
'max' => 1000000,
], // Represents EUR 1 - 10,000 EUR.
Country_Code::IRELAND => [
'min' => 100,
'max' => 400000,
], // Represents EUR 1 - 4,000 EUR.
Country_Code::ITALY => [
'min' => 100,
'max' => 400000,
], // Represents EUR 1 - 4,000 EUR.
Country_Code::FRANCE => [
'min' => 100,
'max' => 400000,
], // Represents EUR 1 - 4,000 EUR.
],
Currency_Code::DANISH_KRONE => [
Country_Code::DENMARK => [
'min' => 100,
'max' => 10000000,
], // Represents DKK 1 - 100,000 DKK.
],
Currency_Code::NORWEGIAN_KRONE => [
Country_Code::NORWAY => [
'min' => 100,
'max' => 10000000,
], // Represents NOK 1 - 100,000 NOK.
],
Currency_Code::SWEDISH_KRONA => [
Country_Code::SWEDEN => [
'min' => 100,
'max' => 10000000,
], // Represents SEK 1 - 100,000 SEK.
],
];
default:
$minimum_amount = 50;
break;
return [];
}
}

self::cache_minimum_amount( $currency, $minimum_amount );
/**
* Check if any BNPL method is available for a given country, currency, and price.
*
* @param array $enabled_methods Array of enabled BNPL methods.
* @param string $country_code Country code.
* @param string $currency_code Currency code.
* @param float $price Product price.
* @return bool True if any BNPL method is available, false otherwise.
*/
public static function is_any_bnpl_method_available( array $enabled_methods, string $country_code, string $currency_code, float $price ): bool {
$price_in_cents = $price;

foreach ( $enabled_methods as $method ) {
$limits = self::get_bnpl_limits_per_currency( $method );

return $minimum_amount;
if ( isset( $limits[ $currency_code ][ $country_code ] ) ) {
$min_amount = $limits[ $currency_code ][ $country_code ]['min'];
$max_amount = $limits[ $currency_code ][ $country_code ]['max'];

if ( $price_in_cents >= $min_amount && $price_in_cents <= $max_amount ) {
return true;
}
}
}

return false;
}

/**
Expand All @@ -785,20 +889,12 @@ public static function cache_minimum_amount( $currency, $amount ) {
* Checks if there is a minimum amount required for transactions in a given currency.
*
* @param string $currency The currency to check for.
* @param bool $fallback_to_local_list Whether to fallback to the local Stripe list if the cached value is not available.
*
* @return int|null Either the minimum amount, or `null` if not available.
*/
public static function get_cached_minimum_amount( $currency, $fallback_to_local_list = false ) {
public static function get_cached_minimum_amount( $currency ) {
$cached = get_transient( 'wcpay_minimum_amount_' . strtolower( $currency ) );

if ( (int) $cached ) {
return (int) $cached;
} elseif ( $fallback_to_local_list ) {
return self::get_stripe_minimum_amount( $currency );
}

return null;
return (int) $cached ? (int) $cached : null;
}
Comment on lines +895 to 898
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, reverting this back to it's state prior to #9355.


/**
Expand Down
Loading
Loading