Skip to content

Commit

Permalink
Implement gateway method to retrieve recommended PMs (#9825)
Browse files Browse the repository at this point in the history
Co-authored-by: Dan Paun <[email protected]>
Co-authored-by: Vlad Olaru <[email protected]>
Co-authored-by: Vlad Olaru <[email protected]>
  • Loading branch information
4 people authored Dec 6, 2024
1 parent 007d6d6 commit 81ee27e
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 1 deletion.
4 changes: 4 additions & 0 deletions changelog/add-9690-recommended-pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Implement gateway method to retrieve recommended payment method.
1 change: 1 addition & 0 deletions includes/class-database-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Database_Cache implements MultiCurrencyCacheInterface {
const BUSINESS_TYPES_KEY = 'wcpay_business_types_data';
const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors';
const FRAUD_SERVICES_KEY = 'wcpay_fraud_services_data';
const RECOMMENDED_PAYMENT_METHODS = 'wcpay_recommended_payment_methods';

/**
* Refresh during AJAX calls is avoided, but white-listing
Expand Down
31 changes: 30 additions & 1 deletion includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -4498,11 +4498,40 @@ public function find_duplicates() {
return $this->duplicate_payment_methods_detection_service->find_duplicates();
}

/**
* Get the recommended payment methods list.
*
* @param string $country_code Optional. The business location country code. Provide a 2-letter ISO country code.
* If not provided, the account country will be used if the account is connected.
* Otherwise, the store's base country will be used.
*
* @return array List of recommended payment methods for the given country.
* Empty array if there are no recommendations available.
* Each item in the array should be an associative array with at least the following entries:
* - @string id: The payment method ID.
* - @string title: The payment method title/name.
* - @bool enabled: Whether the payment method is enabled.
* - @int order/priority: The order/priority of the payment method.
*/
public function get_recommended_payment_methods( string $country_code = '' ): array {
if ( empty( $country_code ) ) {
// If the account is connected, use the account country.
if ( $this->account->is_provider_connected() ) {
$country_code = $this->get_account_country();
} else {
// If the account is not connected, use the store's base country.
$country_code = WC()->countries->get_base_country();
}
}

return $this->account->get_recommended_payment_methods( $country_code );
}

/**
* Determine whether redirection is needed for the non-card UPE payment method.
*
* @param array $payment_methods The list of payment methods used for the order processing, usually consists of one method only.
* @return boolean True if the arrray consist of only one payment method which is not a card. False otherwise.
* @return boolean True if the array consist of only one payment method which is not a card. False otherwise.
*/
private function upe_needs_redirection( $payment_methods ) {
return 1 === count( $payment_methods ) && 'card' !== $payment_methods[0];
Expand Down
63 changes: 63 additions & 0 deletions includes/class-wc-payments-account.php
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,69 @@ public function get_supported_countries(): array {
return WC_Payments_Utils::supported_countries();
}

/**
* Get the account recommended payment methods to use during onboarding.
*
* @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
*
* @return array List of recommended payment methods for the given country.
* Empty array if there are no recommendations, we failed to retrieve recommendations,
* or the country is not supported by WooPayments.
*/
public function get_recommended_payment_methods( string $country_code ): array {
// Return early if the country is not supported.
if ( ! array_key_exists( $country_code, $this->get_supported_countries() ) ) {
return [];
}

// We use the locale for the current user (defaults to the site locale).
$recommended_pms = $this->onboarding_service->get_recommended_payment_methods( $country_code, get_user_locale() );
$recommended_pms = is_array( $recommended_pms ) ? array_values( $recommended_pms ) : [];

// Validate the recommended payment methods.
// Each must have an ID and a title.
$recommended_pms = array_filter(
$recommended_pms,
function ( $pm ) {
return isset( $pm['id'] ) && isset( $pm['title'] );
}
);

// Standardize/normalize.
// Determine if the payment method should be recommended as enabled.
$recommended_pms = array_map(
function ( $pm ) {
if ( ! isset( $pm['enabled'] ) ) {
// Default to enabled since this is a recommended list.
$pm['enabled'] = true;
// Look at the type, if available, to determine if it should be enabled.
if ( isset( $pm['type'] ) ) {
$pm['enabled'] = 'available' !== $pm['type'];
}
}

return $pm;
},
$recommended_pms
);
// Fill in the priority entries with a fallback to the index of the recommendation in the list.
$recommended_pms = array_map(
function ( $pm, $index ) {
if ( ! isset( $pm['priority'] ) ) {
$pm['priority'] = $index;
} else {
$pm['priority'] = intval( $pm['priority'] );
}

return $pm;
},
$recommended_pms,
array_keys( $recommended_pms )
);

return $recommended_pms;
}

/**
* Gets the account live mode value.
*
Expand Down
29 changes: 29 additions & 0 deletions includes/class-wc-payments-onboarding-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ function () use ( $locale ) {
);
}

/**
* Retrieve and cache the account recommended payment methods list.
*
* @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
* @param string $locale Optional. The locale to use to i18n the data.
*
* @return ?array The recommended payment methods list.
* NULL on retrieval or validation error.
*/
public function get_recommended_payment_methods( string $country_code, string $locale = '' ): ?array {
$cache_key = Database_Cache::RECOMMENDED_PAYMENT_METHODS . '__' . $country_code;
if ( ! empty( $locale ) ) {
$cache_key .= '__' . $locale;
}

return \WC_Payments::get_database_cache()->get_or_add(
$cache_key,
function () use ( $country_code, $locale ) {
try {
return $this->payments_api_client->get_recommended_payment_methods( $country_code, $locale );
} catch ( API_Exception $e ) {
// Return NULL to signal retrieval error.
return null;
}
},
'is_array'
);
}

/**
* Retrieve the embedded KYC session and handle initial account creation (if necessary).
*
Expand Down
60 changes: 60 additions & 0 deletions includes/wc-payment-api/class-wc-payments-api-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class WC_Payments_API_Client implements MultiCurrencyApiClientInterface {
const FRAUD_RULESET_API = 'fraud_ruleset';
const COMPATIBILITY_API = 'compatibility';
const REPORTING_API = 'reporting/payment_activity';
const RECOMMENDED_PAYMENT_METHODS = 'payment_methods/recommended';

/**
* Common keys in API requests/responses that we might want to redact.
Expand Down Expand Up @@ -453,6 +454,65 @@ public function get_transactions_export( $filters = [], $user_email = '', $depos
return $this->request( $filters, self::TRANSACTIONS_API . '/download', self::POST );
}

/**
* Fetch account recommended payment methods data for a given country.
*
* @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
* @param string $locale Optional. The locale to instruct the platform to use for i18n.
*
* @return array The recommended payment methods data.
* @throws API_Exception Exception thrown on request failure.
*/
public function get_recommended_payment_methods( string $country_code, string $locale = '' ): array {
// We can't use the request method here because this route doesn't require a connected store
// and we request this data pre-onboarding.
// By this point, we have an expired transient or the store context has changed.
// Query for incentives by calling the WooPayments API.
$url = add_query_arg(
[
'country_code' => $country_code,
'locale' => $locale,
],
self::ENDPOINT_BASE . '/' . self::ENDPOINT_REST_BASE . '/' . self::RECOMMENDED_PAYMENT_METHODS,
);

$response = wp_remote_get(
$url,
[
'headers' => apply_filters(
'wcpay_api_request_headers',
[
'Content-type' => 'application/json; charset=utf-8',
]
),
'user-agent' => $this->user_agent,
'timeout' => self::API_TIMEOUT_SECONDS,
'sslverify' => false,
]
);

if ( is_wp_error( $response ) ) {
Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
$message = sprintf(
// translators: %1: original error message.
__( 'Http request failed. Reason: %1$s', 'woocommerce-payments' ),
$response->get_error_message()
);
throw new API_Exception( $message, 'wcpay_http_request_failed', 500 );
}

$results = [];
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
// Decode the results, falling back to an empty array.
$results = $this->extract_response_body( $response );
if ( ! is_array( $results ) ) {
$results = [];
}
}

return $results;
}

/**
* Fetch a single transaction with provided id.
*
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test-class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -3963,6 +3963,57 @@ public function is_proper_intent_used_with_order_returns_false() {
$this->assertFalse( $this->card_gateway->is_proper_intent_used_with_order( WC_Helper_Order::create_order(), 'wrong_intent_id' ) );
}

public function test_get_recommended_payment_method() {
$this->mock_wcpay_account
->expects( $this->once() )
->method( 'get_recommended_payment_methods' )
->with( 'US' );
$this->card_gateway->get_recommended_payment_methods( 'US' );
}

public function get_recommended_payment_method_no_country_code_provider() {
return [
'provider connected' => [ true, 'test' ],
'provider not connected' => [ false, 'US' ],
];
}

/**
* @dataProvider get_recommended_payment_method_no_country_code_provider
*/
public function test_get_recommended_payment_method_no_country_code_provided( $is_provider_connected, $country_code ) {
// Set base country fallback to US.
$filter_callback = function () {
return 'US';
};
add_filter( 'woocommerce_countries_base_country', $filter_callback );

$this->mock_wcpay_account
->expects( $this->once() )
->method( 'is_provider_connected' )
->willReturn( $is_provider_connected );

$this->mock_wcpay_account
->expects( $this->any() )
->method( 'is_stripe_connected' )
->willReturn( true );

$this->mock_wcpay_account
->expects( $this->any() )
->method( 'get_account_country' )
->willReturn( $country_code );

$this->mock_wcpay_account
->expects( $this->once() )
->method( 'get_recommended_payment_methods' )
->with( $country_code );

$this->assertSame( [], $this->card_gateway->get_recommended_payment_methods( '' ) );

// Clean up.
remove_filter( 'woocommerce_countries_base_country', $filter_callback );
}

/**
* Sets up the expectation for a certain factor for the new payment
* process to be either set or unset.
Expand Down
85 changes: 85 additions & 0 deletions tests/unit/test-class-wc-payments-account.php
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,91 @@ public function test_get_tracking_info() {
$this->assertSame( $expected, $this->wcpay_account->get_tracking_info() );
}

public function test_get_recommended_payment_methods_unsupported_country() {
$this->assertSame( [], $this->wcpay_account->get_recommended_payment_methods( 'XZ' ) );
}

public function get_recommended_payment_methods_provider() {
return [
'No PMs suggested' => [ 'US', [], [] ],
'Invalid PMs array' => [
'US',
[
'type' => 'available',
'enabled' => false,
],
[],
],
'Enabled flag and priority not set' => [
'US',
[
[
'id' => 1,
'title' => 'test PM',
'type' => 'available',
],
[
'id' => 2,
'title' => 'test PM 2',
'type' => 'available',
],
],
[
[
'id' => 1,
'title' => 'test PM',
'type' => 'available',
'enabled' => false,
'priority' => 0,
],
[
'id' => 2,
'title' => 'test PM 2',
'type' => 'available',
'enabled' => false,
'priority' => 1,
],
],
],
'Enabled flag and priority set' => [
'US',
[
[
'id' => 1,
'title' => 'test PM',
'type' => 'available',
'enabled' => true,
'priority' => 1,
],
],
[
[
'id' => 1,
'title' => 'test PM',
'type' => 'available',
'enabled' => true,
'priority' => 1,
],
],
],
];
}

/**
* @dataProvider get_recommended_payment_methods_provider
*/
public function test_get_recommended_payment_methods( $country_code, $recommended_pms, $expected ) {

$this->mock_empty_cache();
$this->mock_onboarding_service
->expects( $this->once() )
->method( 'get_recommended_payment_methods' )
->with( $country_code )
->willReturn( $recommended_pms );

$this->assertSame( $expected, $this->wcpay_account->get_recommended_payment_methods( $country_code ) );
}

/**
* Sets up the mocked cache to simulate that its empty and call the generator.
*/
Expand Down

0 comments on commit 81ee27e

Please sign in to comment.