Skip to content

Commit

Permalink
Fix: BNPL payment methods should work when available in the Pay For O…
Browse files Browse the repository at this point in the history
…rder page (#9670)
  • Loading branch information
danielmx-dev authored Nov 6, 2024
1 parent c061a08 commit ed04c34
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 6 deletions.
4 changes: 4 additions & 0 deletions changelog/fix-8254-bnpl-pay-for-order
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

BNPL methods now work properly in Pay for Order when they are available. Default values are also provided when available.
45 changes: 44 additions & 1 deletion client/checkout/classic/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,41 @@ function submitForm( jQueryForm ) {
jQueryForm.removeClass( 'processing' ).submit();
}

/**
* Validates the contents of the address fields based on the requirements from BNPL payment methods.
*
* @param {Object} params The parameters to be sent to `createPaymentMethod`.
* @param {string} paymentMethodType The type of Stripe payment method to create.
* @return {boolean} True, if there are missing address fields. False, if the validation passes or is not applicable.
*/
function isMissingRequiredAddressFieldsForBNPL( params, paymentMethodType ) {
if ( [ 'afterpay_clearpay', 'affirm' ].includes( paymentMethodType ) ) {
return false;
}
const address = params?.billing_details?.address;

if ( ! address ) {
return false;
}

const requiredAddressFields =
paymentMethodType === 'affirm'
? [ 'line1', 'state', 'city', 'postal_code', 'country' ] // Line2 is not required.
: [ 'line1', 'postal_code', 'country' ]; // City and State are not required in Afterpay.

for ( const field of requiredAddressFields ) {
if (
address[ field ] === '' ||
address[ field ] === null ||
typeof address[ field ] === 'undefined'
) {
return true;
}
}

return false;
}

/**
* Creates a Stripe payment method by calling the Stripe API's createPaymentMethod with the provided elements
* and billing details. The billing details are obtained from various form elements on the page.
Expand All @@ -144,7 +179,7 @@ function createStripePaymentMethod(
billing_details: {
name: wcpayCustomerData.name || undefined,
email: wcpayCustomerData.email,
address: {
address: wcpayCustomerData.address || {
country: wcpayCustomerData.billing_country,
},
},
Expand Down Expand Up @@ -192,6 +227,14 @@ function createStripePaymentMethod(
};
}

if (
getUPEConfig( 'isOrderPay' ) &&
isMissingRequiredAddressFieldsForBNPL( params, paymentMethodType )
) {
// These payment methods don't accept an address object with partial information, so we just remove the object entirely.
delete params.billing_details.address;
}

return api
.getStripeForUPE( paymentMethodType )
.createPaymentMethod( { elements, params: params } );
Expand Down
30 changes: 30 additions & 0 deletions client/checkout/utils/test/upe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ describe( 'UPE checkout utils', () => {
if ( checkboxElement ) {
checkboxElement.remove();
}

delete window.wcpayCustomerData;
} );

it( 'should not provide terms when cart does not contain subscriptions and the saving checkbox is unchecked', () => {
Expand Down Expand Up @@ -355,6 +357,34 @@ describe( 'UPE checkout utils', () => {
expect( upeSettings.terms.card ).toEqual( 'always' );
} );

it( 'should define defaultValues when wcpayCustomerData is present', () => {
window.wcpayCustomerData = {
name: 'Test Person',
email: '[email protected]',
billing_country: 'US',
};

const upeSettings = getUpeSettings();

expect( upeSettings.defaultValues ).toEqual( {
billingDetails: {
name: 'Test Person',
email: '[email protected]',
address: {
country: 'US',
},
},
} );
} );

it( 'should not define defaultValues if wcpayCustomerData is not present', () => {
window.wcpayCustomerData = null;

const upeSettings = getUpeSettings();

expect( upeSettings.defaultValues ).toBeUndefined();
} );

function createCheckboxElementWhich( isChecked ) {
// Create the checkbox element
const checkboxElement = document.createElement( 'input' );
Expand Down
12 changes: 12 additions & 0 deletions client/checkout/utils/upe.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ export const getUpeSettings = () => {
};
}

if ( window.wcpayCustomerData ) {
upeSettings.defaultValues = {
billingDetails: {
name: window.wcpayCustomerData.name,
email: window.wcpayCustomerData.email,
address: {
country: window.wcpayCustomerData.billing_country,
},
},
};
}

return upeSettings;
};

Expand Down
23 changes: 21 additions & 2 deletions includes/class-wc-payments-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,16 +376,35 @@ public function payment_fields() {
if ( ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) {
$payment_fields = $this->get_payment_fields_js_config();
wp_enqueue_script( 'wcpay-upe-checkout' );
/**
* We can't localize the script right away since at this point is not registered yet.
* We also need to make sure it that it only runs once (using a dummy action), even if
* there are multiple payment methods available; otherwise the data will be overwritten
* which is pointless.
*
* The same applies for `wcpayCustomerData` a few lines below.
*/
add_action(
'wp_footer',
function () use ( $payment_fields ) {
wp_localize_script( 'wcpay-upe-checkout', 'wcpay_upe_config', $payment_fields );
if ( ! did_action( '__wcpay_upe_config_localized' ) ) {
wp_localize_script( 'wcpay-upe-checkout', 'wcpay_upe_config', $payment_fields );
}
do_action( '__wcpay_upe_config_localized' );
}
);

$prepared_customer_data = $this->customer_service->get_prepared_customer_data();
if ( ! empty( $prepared_customer_data ) ) {
wp_localize_script( 'wcpay-upe-checkout', 'wcpayCustomerData', $prepared_customer_data );
add_action(
'wp_footer',
function () use ( $prepared_customer_data ) {
if ( ! did_action( '__wcpay_customer_data_localized' ) ) {
wp_localize_script( 'wcpay-upe-checkout', 'wcpayCustomerData', $prepared_customer_data );
}
do_action( '__wcpay_customer_data_localized' );
}
);
}

WC_Payments_Utils::enqueue_style(
Expand Down
10 changes: 10 additions & 0 deletions includes/class-wc-payments-customer-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ public function get_prepared_customer_data() {
$firstname = '';
$lastname = '';
$billing_country = '';
$address = null;

if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_id = absint( $wp->query_vars['order-pay'] );
Expand All @@ -541,6 +542,14 @@ public function get_prepared_customer_data() {
$lastname = $order->get_billing_last_name();
$user_email = $order->get_billing_email();
$billing_country = $order->get_billing_country();
$address = [
'city' => $order->get_billing_city(),
'country' => $order->get_billing_country(),
'line1' => $order->get_billing_address_1(),
'line2' => $order->get_billing_address_2(),
'postal_code' => $order->get_billing_postcode(),
'state' => $order->get_billing_state(),
];
}
}

Expand All @@ -560,6 +569,7 @@ public function get_prepared_customer_data() {
'name' => $firstname . ' ' . $lastname,
'email' => $user_email,
'billing_country' => $billing_country,
'address' => $address,
];
}
}
23 changes: 20 additions & 3 deletions includes/payment-methods/class-upe-payment-method.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,28 @@ public function is_enabled_at_checkout( string $account_country ) {

// This part ensures that when payment limits for the currency declared, those will be respected (e.g. BNPLs).
if ( [] !== $this->limits_per_currency ) {
$order = null;
if ( is_wc_endpoint_url( 'order-pay' ) ) {
$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
$order = is_a( $order, 'WC_Order' ) ? $order : null;
}

$currency = get_woocommerce_currency();
if ( $order ) {
$currency = $order->get_currency();
}

// If the currency limits are not defined, we allow the PM for now (gateway has similar validation for limits).
// Additionally, we don't engage with limits verification in no-checkout context (cart is not available or empty).
if ( isset( $this->limits_per_currency[ $currency ], WC()->cart ) ) {
$amount = WC_Payments_Utils::prepare_amount( WC()->cart->get_total( '' ), $currency );
$total = null;
if ( $order ) {
$total = $order->get_total();
} elseif ( isset( WC()->cart ) ) {
$total = WC()->cart->get_total( '' );
}

if ( isset( $this->limits_per_currency[ $currency ], WC()->cart ) && ! empty( $total ) ) {
$amount = WC_Payments_Utils::prepare_amount( $total, $currency );

if ( $amount > 0 ) {
$range = null;
if ( isset( $this->limits_per_currency[ $currency ][ $account_country ] ) ) {
Expand Down
96 changes: 96 additions & 0 deletions tests/unit/test-class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ class WC_Payment_Gateway_WCPay_Test extends WCPAY_UnitTestCase {
*/
private $locale_backup;


/**
* Backup of $wp->query_vars
*
* @var array
*/
private $wp_query_vars_backup;

/**
* Backup of $wp_query->query_vars
*
* @var array
*/
private $wp_query_query_vars_backup;

/**
* Pre-test setup
*/
Expand Down Expand Up @@ -281,6 +296,11 @@ public function set_up() {
wcpay_get_test_container()->replace( OrderService::class, $mock_order_service );

$this->locale_backup = WC()->countries->get_country_locale();

global $wp;
global $wp_query;
$this->wp_query_vars_backup = $wp->query_vars;
$this->wp_query_query_vars_backup = $wp_query->query_vars;
}

/**
Expand Down Expand Up @@ -318,6 +338,11 @@ public function tear_down() {
wcpay_get_test_container()->reset_all_replacements();
WC()->session->set( 'wc_notices', [] );
WC()->countries->locale = $this->locale_backup;

global $wp;
global $wp_query;
$wp->query_vars = $this->wp_query_vars_backup;
$wp_query->query_vars = $this->wp_query_query_vars_backup;
}

public function test_process_redirect_payment_intent_processing() {
Expand Down Expand Up @@ -706,6 +731,77 @@ function ( $order ) {
$this->assertFalse( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}

public function test_payment_methods_enabled_based_on_currency_limits() {
WC_Helper_Site_Currency::$mock_site_currency = 'USD';

WC()->session->init();
WC()->cart->empty_cart();
// Total is 100 USD, which is above both payment methods (Affirm and AfterPay) minimums.
WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 10 );
WC()->cart->calculate_totals();

$affirm_method = $this->payment_methods['affirm'];
$afterpay_method = $this->payment_methods['afterpay_clearpay'];

$this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US' ) );
$this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}

public function test_payment_methods_disabled_based_on_currency_limits() {
WC_Helper_Site_Currency::$mock_site_currency = 'USD';

WC()->session->init();
WC()->cart->empty_cart();
// Total is 40 USD, which is below Affirm minimum.
WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id(), 4 );
WC()->cart->calculate_totals();

$affirm_method = $this->payment_methods['affirm'];
$afterpay_method = $this->payment_methods['afterpay_clearpay'];

$this->assertFalse( $affirm_method->is_enabled_at_checkout( 'US' ) );
$this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}

public function test_payment_methods_enabled_based_on_currency_limits_in_order_pay() {
global $wp;
global $wp_query;

WC_Helper_Site_Currency::$mock_site_currency = 'USD';

// Total is 100 USD, which is above both payment methods (Affirm and AfterPay) minimums.
$order = WC_Helper_Order::create_order( 1, 100 );
$order_id = $order->get_id();
$wp->query_vars = [ 'order-pay' => strval( $order_id ) ];
$wp_query->query_vars = [ 'order-pay' => strval( $order_id ) ];

$affirm_method = $this->payment_methods['affirm'];
$afterpay_method = $this->payment_methods['afterpay_clearpay'];

$this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US' ) );
$this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}

public function test_payment_methods_disabled_based_on_currency_limits_in_order_pay() {
global $wp;
global $wp_query;

WC_Helper_Site_Currency::$mock_site_currency = 'USD';

// Total is 40 USD, which is below Affirm minimum.
$order = WC_Helper_Order::create_order( 1, 40 );
$order_id = $order->get_id();
$wp->query_vars = [ 'order-pay' => strval( $order_id ) ];
$wp_query->query_vars = [ 'order-pay' => strval( $order_id ) ];
$order->set_currency( 'USD' );

$affirm_method = $this->payment_methods['affirm'];
$afterpay_method = $this->payment_methods['afterpay_clearpay'];

$this->assertFalse( $affirm_method->is_enabled_at_checkout( 'US' ) );
$this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) );
}

public function test_only_valid_payment_methods_returned_for_currency() {
$card_method = $this->payment_methods['card'];
$giropay_method = $this->payment_methods['giropay'];
Expand Down

0 comments on commit ed04c34

Please sign in to comment.