diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ec73e16fd..8f31382e3 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; @@ -120,7 +121,8 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'api.factory.sellerstatus' ), $container->get( 'api.partner_merchant_id' ), - $container->get( 'api.merchant_id' ) + $container->get( 'api.merchant_id' ), + $container->get( 'api.helper.failure-registry' ) ); }, 'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory { @@ -846,6 +848,10 @@ return array( $purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); return new OrderTransient( $cache, $purchase_unit_sanitizer ); }, + 'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry { + $cache = new Cache( 'ppcp-paypal-api-status-cache' ); + return new FailureRegistry( $cache ); + }, 'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make( static function( ContainerInterface $container ): PurchaseUnitSanitizer { $settings = $container->get( 'wcgateway.settings' ); diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php index f12e69668..ed09ecddc 100644 --- a/modules/ppcp-api-client/src/ApiModule.php +++ b/modules/ppcp-api-client/src/ApiModule.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; @@ -81,6 +82,18 @@ class ApiModule implements ModuleInterface { 10, 2 ); + add_action( + 'woocommerce_paypal_payments_clear_apm_product_status', + function () use ( $c ) { + $failure_registry = $c->has( 'api.helper.failure-registry' ) ? $c->get( 'api.helper.failure-registry' ) : null; + + if ( $failure_registry instanceof FailureRegistry ) { + $failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); + } + }, + 10, + 2 + ); } /** diff --git a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php index 275f94c97..0172f8af1 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; /** * Class PartnersEndpoint @@ -65,6 +66,13 @@ class PartnersEndpoint { */ private $merchant_id; + /** + * The failure registry. + * + * @var FailureRegistry + */ + private $failure_registry; + /** * PartnersEndpoint constructor. * @@ -74,6 +82,7 @@ class PartnersEndpoint { * @param SellerStatusFactory $seller_status_factory The seller status factory. * @param string $partner_id The partner ID. * @param string $merchant_id The merchant ID. + * @param FailureRegistry $failure_registry The API failure registry. */ public function __construct( string $host, @@ -81,7 +90,8 @@ class PartnersEndpoint { LoggerInterface $logger, SellerStatusFactory $seller_status_factory, string $partner_id, - string $merchant_id + string $merchant_id, + FailureRegistry $failure_registry ) { $this->host = $host; $this->bearer = $bearer; @@ -89,6 +99,7 @@ class PartnersEndpoint { $this->seller_status_factory = $seller_status_factory; $this->partner_id = $partner_id; $this->merchant_id = $merchant_id; + $this->failure_registry = $failure_registry; } /** @@ -125,9 +136,15 @@ class PartnersEndpoint { 'response' => $response, ) ); + + // Register the failure on api failure registry. + $this->failure_registry->add_failure( FailureRegistry::SELLER_STATUS_KEY ); + throw $error; } + $this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); + $json = json_decode( wp_remote_retrieve_body( $response ) ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { diff --git a/modules/ppcp-api-client/src/Helper/FailureRegistry.php b/modules/ppcp-api-client/src/Helper/FailureRegistry.php new file mode 100644 index 000000000..a7919758c --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/FailureRegistry.php @@ -0,0 +1,88 @@ +cache = $cache; + } + + /** + * @param string $key + * @param int $seconds + * @return bool + */ + public function has_failure_in_timeframe( string $key, int $seconds ): bool { + $cache_key = $this->cache_key( $key ); + $failure_time = $this->cache->get( $cache_key ); + + if ( ! $failure_time ) { + return false; + } + + $expiration = $failure_time + $seconds; + return $expiration > time(); + } + + /** + * @param string $key + * @return void + */ + public function add_failure( string $key ) { + $cache_key = $this->cache_key( $key ); + $this->cache->set( $cache_key, time(), self::CACHE_TIMEOUT ); + } + + /** + * @param string $key + * @return void + */ + public function clear_failures( string $key ) { + $cache_key = $this->cache_key( $key ); + if ( $this->cache->has( $cache_key ) ) { + $this->cache->delete( $cache_key ); + } + } + + /** + * Build cache key. + * + * @param string $key The cache key. + * @return string|null + */ + private function cache_key( string $key ): ?string { + return implode( '_', array( self::CACHE_KEY, $key ) ); + } + +} diff --git a/modules/ppcp-api-client/src/Helper/OrderTransient.php b/modules/ppcp-api-client/src/Helper/OrderTransient.php index d0c7d4a01..09dac1af9 100644 --- a/modules/ppcp-api-client/src/Helper/OrderTransient.php +++ b/modules/ppcp-api-client/src/Helper/OrderTransient.php @@ -17,7 +17,7 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; /** - * Class OrderHelper + * Class OrderTransient */ class OrderTransient { const CACHE_KEY = 'order_transient'; diff --git a/modules/ppcp-googlepay/services.php b/modules/ppcp-googlepay/services.php index fafc41de4..958fbbffb 100644 --- a/modules/ppcp-googlepay/services.php +++ b/modules/ppcp-googlepay/services.php @@ -61,7 +61,8 @@ return array( return new ApmProductStatus( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ), - $container->get( 'onboarding.state' ) + $container->get( 'onboarding.state' ), + $container->get( 'api.helper.failure-registry' ) ); } ), diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index eab601704..e6941670d 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -40,6 +40,17 @@ class GooglepayModule implements ModuleInterface { */ public function run( ContainerInterface $c ): void { + // Clears product status when appropriate. + add_action( + 'woocommerce_paypal_payments_clear_apm_product_status', + function( Settings $settings = null ) use ( $c ): void { + $apm_status = $c->get( 'googlepay.helpers.apm-product-status' ); + assert( $apm_status instanceof ApmProductStatus ); + + $apm_status->clear( $settings ); + } + ); + // Check if the module is applicable, correct country, currency, ... etc. if ( ! $c->get( 'googlepay.eligible' ) ) { return; @@ -113,22 +124,6 @@ class GooglepayModule implements ModuleInterface { return $settings; } ); - - // Clears product status when appropriate. - add_action( - 'woocommerce_paypal_payments_clear_apm_product_status', - function( Settings $settings = null ) use ( $c ): void { - $apm_status = $c->get( 'googlepay.helpers.apm-product-status' ); - assert( $apm_status instanceof ApmProductStatus ); - - if ( ! $settings instanceof Settings ) { - $settings = null; - } - - $apm_status->clear( $settings ); - } - ); - } /** diff --git a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php index ef93d8d54..cf1e6487c 100644 --- a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php +++ b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper; use Throwable; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -60,21 +61,31 @@ class ApmProductStatus { */ private $onboarding_state; + /** + * The API failure registry + * + * @var FailureRegistry + */ + private $api_failure_registry; + /** * ApmProductStatus constructor. * * @param Settings $settings The Settings. * @param PartnersEndpoint $partners_endpoint The Partner Endpoint. * @param State $onboarding_state The onboarding state. + * @param FailureRegistry $api_failure_registry The API failure registry. */ public function __construct( Settings $settings, PartnersEndpoint $partners_endpoint, - State $onboarding_state + State $onboarding_state, + FailureRegistry $api_failure_registry ) { - $this->settings = $settings; - $this->partners_endpoint = $partners_endpoint; - $this->onboarding_state = $onboarding_state; + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + $this->onboarding_state = $onboarding_state; + $this->api_failure_registry = $api_failure_registry; } /** @@ -83,34 +94,47 @@ class ApmProductStatus { * @return bool */ public function is_active() : bool { + + // If not onboarded then makes no sense to check status. if ( ! $this->is_onboarded() ) { return false; } + // If status was already checked on this request return the same result. if ( null !== $this->current_status ) { return $this->current_status; } + // Check if status was checked on previous requests. if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) { $this->current_status = wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) ); return $this->current_status; } - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( Throwable $error ) { - // It may be a transitory error, don't persist the status. + // Check API failure registry to prevent multiple failed API requests. + if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) { $this->has_request_failure = true; $this->current_status = false; return $this->current_status; } + // Request seller status via PayPal API. + try { + $seller_status = $this->partners_endpoint->seller_status(); + } catch ( Throwable $error ) { + $this->has_request_failure = true; + $this->current_status = false; + return $this->current_status; + } + + // Check the seller status for the intended capability. foreach ( $seller_status->products() as $product ) { if ( $product->name() !== 'PAYMENT_METHODS' ) { continue; } if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) { + // Capability found, persist status and return true. $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); $this->settings->persist(); @@ -119,6 +143,7 @@ class ApmProductStatus { } } + // Capability not found, persist status and return false. $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED ); $this->settings->persist(); @@ -158,8 +183,10 @@ class ApmProductStatus { $this->current_status = null; - $settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED ); - $settings->persist(); + if ( $settings->has( self::SETTINGS_KEY ) ) { + $settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED ); + $settings->persist(); + } } }