From 82be8112714dfbaf02a105feb36de703a132907f Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 09:59:22 +0300 Subject: [PATCH] validate if credit card can be used in a country --- .../src/Helper/class-dccapplies.php | 237 +++++++++++++++++- .../js/modules/Renderer/CreditCardRenderer.js | 14 +- modules/ppcp-button/services.php | 11 +- .../src/Assets/class-smartbutton.php | 5 + .../Endpoint/class-approveorderendpoint.php | 22 +- modules/ppcp-wc-gateway/services.php | 20 ++ 6 files changed, 298 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-api-client/src/Helper/class-dccapplies.php b/modules/ppcp-api-client/src/Helper/class-dccapplies.php index ddc259646..166e6520b 100644 --- a/modules/ppcp-api-client/src/Helper/class-dccapplies.php +++ b/modules/ppcp-api-client/src/Helper/class-dccapplies.php @@ -38,6 +38,24 @@ class DccApplies { 'SGD', 'USD', ), + 'AT' => array( + 'AUD', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'GBP', + 'HKD', + 'HUF', + 'JPY', + 'NOK', + 'NZD', + 'PLN', + 'SEK', + 'SGD', + 'USD', + ), 'BE' => array( 'AUD', 'CAD', @@ -74,6 +92,24 @@ class DccApplies { 'SGD', 'USD', ), + 'CA' => array( + 'AUD', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'GBP', + 'HKD', + 'HUF', + 'JPY', + 'NOK', + 'NZD', + 'PLN', + 'SEK', + 'SGD', + 'USD', + ), 'CY' => array( 'AUD', 'CAD', @@ -517,14 +553,148 @@ class DccApplies { ); + /** + * Which countries support which credit cards. Empty credit card arrays mean no restriction on + * currency. Otherwise only the currencies in the array are supported. + * + * @var array + */ + private $country_card_matrix = array( + 'AU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'AT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'BE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'CA' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'CAD' ), + 'jcb' => array( 'CAD' ), + ), + 'CY' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'CZ' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'DK' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'EE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'FI' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'FR' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'GR' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'HU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'IT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'LV' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LI' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'MT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'NL' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'NO' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'PL' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'PT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'RO' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'SK' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'SI' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'ES' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'SE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'US' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'USD' ), + 'discover' => array( 'USD' ), + ), + 'GB' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'GBP', 'USD' ), + ), + ); + /** * Returns whether DCC can be used in the current country and the current currency used. * * @return bool */ public function for_country_currency(): bool { - $region = wc_get_base_location(); - $country = $region['country']; + $country = $this->country(); $currency = get_woocommerce_currency(); if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { return false; @@ -532,4 +702,67 @@ class DccApplies { $applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true ); return $applies; } + + /** + * Returns credit cards, which can be used. + * + * @return array + */ + public function valid_cards() : array { + $country = $this->country(); + $cards = array(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return $cards; + } + + $supported_currencies = $this->country_card_matrix[ $country ]; + foreach ( $supported_currencies as $card => $currencies ) { + if ( $this->can_process_card( $card ) ) { + $cards[] = $card; + } + } + if ( in_array( 'amex', $cards, true ) ) { + $cards[] = 'american-express'; + } + if ( in_array( 'mastercard', $cards, true ) ) { + $cards[] = 'master-card'; + } + return $cards; + } + + /** + * Whether a card can be used or not. + * + * @param string $card The card. + * + * @return bool + */ + public function can_process_card( string $card ) : bool { + $country = $this->country(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return false; + } + if ( ! isset( $this->country_card_matrix[ $country ][ $card ] ) ) { + return false; + } + + /** + * If the supported currencies array is empty, there are no + * restrictions, which currencies are supported by a card. + */ + $supported_currencies = $this->country_card_matrix[ $country ][ $card ]; + $currency = get_woocommerce_currency(); + return empty( $supported_currencies ) || in_array( $currency, $supported_currencies, true ); + } + + /** + * Returns the country code of the shop. + * + * @return string + */ + private function country() : string { + $region = wc_get_base_location(); + $country = $region['country']; + return $country; + } } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 56c7d7977..b64892ba9 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -5,6 +5,7 @@ class CreditCardRenderer { constructor(defaultConfig, errorHandler) { this.defaultConfig = defaultConfig; this.errorHandler = errorHandler; + this.cardValid = false; } render(wrapper, contextConfig) { @@ -79,7 +80,7 @@ class CreditCardRenderer { return state.fields[key].isValid; }); - if (formValid) { + if (formValid && this.cardValid) { let vault = document.querySelector(wrapper + ' .ppcp-credit-card-vault') ? document.querySelector(wrapper + ' .ppcp-credit-card-vault').checked : false; @@ -93,12 +94,21 @@ class CreditCardRenderer { return contextConfig.onApprove(payload); }); } else { - this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); + const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + this.errorHandler.message(message); } } hostedFields.on('inputSubmitRequest', function () { submitEvent(null); }); + hostedFields.on('cardTypeChange', (event) => { + if ( ! event.cards.length ) { + this.cardValid = false; + return; + } + const validCards = this.defaultConfig.hosted_fields.valid_cards; + this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; + }) document.querySelector(wrapper + ' button').addEventListener( 'click', submitEvent diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 4bab9ceb3..e5ae5c14a 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; -use Dhii\Data\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; @@ -138,7 +137,15 @@ return array( $session_handler = $container->get( 'session.handler' ); $three_d_secure = $container->get( 'button.helper.three-d-secure' ); $settings = $container->get( 'wcgateway.settings' ); - return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure, $settings ); + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); + return new ApproveOrderEndpoint( + $request_data, + $order_endpoint, + $session_handler, + $three_d_secure, + $settings, + $dcc_applies + ); }, 'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint { $request_data = $container->get( 'button.request-data' ); diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 56b8c23df..db4fc7e48 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -601,7 +601,12 @@ class SmartButton implements SmartButtonInterface { 'Unfortunatly, your credit card details are not valid.', 'paypal-payments-for-woocommerce' ), + 'card_not_supported' => __( + 'Unfortunatly, we do not support your credit card.', + 'paypal-payments-for-woocommerce' + ), ), + 'valid_cards' => $this->dcc_applies->valid_cards(), ), 'messages' => $this->message_values(), 'labels' => array( diff --git a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php index dc819f1e2..783618c43 100644 --- a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php +++ b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php @@ -13,6 +13,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -61,6 +62,13 @@ class ApproveOrderEndpoint implements EndpointInterface { */ private $settings; + /** + * The DCC applies object. + * + * @var DccApplies + */ + private $dcc_applies; + /** * ApproveOrderEndpoint constructor. * @@ -69,13 +77,15 @@ class ApproveOrderEndpoint implements EndpointInterface { * @param SessionHandler $session_handler The session handler. * @param ThreeDSecure $three_d_secure The 3d secure helper object. * @param Settings $settings The settings. + * @param DccApplies $dcc_applies The DCC applies object. */ public function __construct( RequestData $request_data, OrderEndpoint $order_endpoint, SessionHandler $session_handler, ThreeDSecure $three_d_secure, - Settings $settings + Settings $settings, + DccApplies $dcc_applies ) { $this->request_data = $request_data; @@ -83,6 +93,7 @@ class ApproveOrderEndpoint implements EndpointInterface { $this->session_handler = $session_handler; $this->threed_secure = $three_d_secure; $this->settings = $settings; + $this->dcc_applies = $dcc_applies; } /** @@ -123,11 +134,12 @@ class ApproveOrderEndpoint implements EndpointInterface { if ( $order->payment_source() && $order->payment_source()->card() ) { if ( $this->settings->has( 'disable_cards' ) ) { $disabled_cards = (array) $this->settings->get( 'disable_cards' ); - if ( in_array( 'mastercard', $disabled_cards, true ) ) { - $disabled_cards[] = 'master_card'; + $card = strtolower( $order->payment_source()->card()->brand() ); + if ( 'master_card' === $card ) { + $card = 'mastercard'; } - $card = strtolower( $order->payment_source()->card()->brand() ); - if ( in_array( $card, $disabled_cards, true ) ) { + + if ( ! $this->dcc_applies->can_process_card( $card ) || in_array( $card, $disabled_cards, true ) ) { throw new RuntimeException( __( 'Unfortunately, we do not accept this card.', diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 0dba777cd..92b816b41 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway; use Dhii\Data\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn; use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail; @@ -1581,6 +1582,25 @@ return array( if ( 'GB' === $country ) { unset( $fields['disable_funding']['options']['card'] ); } + + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); + /** + * Depending on your store location, some credit cards can't be used. + * Here, we filter them out. + * + * The DCC Applies object. + * + * @var DccApplies $dcc_applies + */ + $card_options = $fields['disable_cards']['options']; + foreach ( $card_options as $card => $label ) { + if ( $dcc_applies->can_process_card( $card ) ) { + continue; + } + unset( $card_options[ $card ] ); + } + $fields['disable_cards']['options'] = $card_options; + $fields['card_icons']['options'] = $card_options; return $fields; },