From 1581f34a44d19373713e2375a1e09d094784c8a6 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 22 Sep 2023 16:27:43 +0100
Subject: [PATCH] Add failure registry to API module. Add product status
failure handling to GooglePay.
---
modules/ppcp-api-client/services.php | 8 +-
modules/ppcp-api-client/src/ApiModule.php | 13 +++
.../src/Endpoint/PartnersEndpoint.php | 19 +++-
.../src/Helper/FailureRegistry.php | 88 +++++++++++++++++++
.../src/Helper/OrderTransient.php | 2 +-
modules/ppcp-googlepay/services.php | 3 +-
.../ppcp-googlepay/src/GooglepayModule.php | 27 +++---
.../src/Helper/ApmProductStatus.php | 47 +++++++---
8 files changed, 177 insertions(+), 30 deletions(-)
create mode 100644 modules/ppcp-api-client/src/Helper/FailureRegistry.php
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();
+ }
}
}