Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/seller-status-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Seller Status Filter

## Overview
Two WordPress filters allow external plugins to override or replace the PayPal merchant-integrations API response. This is useful when the API is unavailable (e.g., Stage environment returning 404) or when testing specific feature eligibility.

## Filters

### `woocommerce_paypal_payments_seller_status`
Filters the `SellerStatus` object on every return from `PartnersEndpoint::seller_status()` — whether from cache or a fresh API call.

```php
add_filter( 'woocommerce_paypal_payments_seller_status', function ( SellerStatus $status ): SellerStatus {
// Modify and return.
return $status;
} );
```

### `woocommerce_paypal_payments_seller_status_fallback`
Provides a fallback `SellerStatus` when the API call fails. Return a `SellerStatus` to use it; return `null` to let the exception propagate.

```php
add_filter( 'woocommerce_paypal_payments_seller_status_fallback', function () {
return new SellerStatus( array(), array(), 'US' );
} );
```

## Example: Enable ACDC + Vaulting on Stage
```php
add_filter( 'woocommerce_paypal_payments_seller_status_fallback', function () {
return new SellerStatus( array(), array(), 'US' );
} );

add_filter( 'woocommerce_paypal_payments_seller_status', function ( SellerStatus $status ): SellerStatus {
// Build modified products/capabilities and return a new SellerStatus.
} );
```
59 changes: 53 additions & 6 deletions modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,60 @@ public function __construct(
public function seller_status(): SellerStatus {
$cached = $this->cache->get( self::SELLER_STATUS_CACHE_KEY );
if ( $cached instanceof SellerStatus ) {
return $cached;
/**
* Filters the seller status object before it is returned.
*
* @param SellerStatus $status The seller status (from cache or API).
*/
return apply_filters( 'woocommerce_paypal_payments_seller_status', $cached );
}

try {
$status = $this->fetch_seller_status_from_api();
} catch ( RuntimeException $exception ) {
/**
* Provides a fallback SellerStatus when the API call fails.
*
* Return a SellerStatus instance to use as fallback instead of
* throwing. Return null to let the exception propagate.
*
* @param SellerStatus|null $fallback Default null (no fallback).
*/
$fallback = apply_filters( 'woocommerce_paypal_payments_seller_status_fallback', null );

if ( $fallback instanceof SellerStatus ) {
$this->logger->log(
'info',
'Seller status API failed, using configured fallback.',
array( 'error' => $exception->getMessage() )
);

/** @var SellerStatus $status */
$status = apply_filters( 'woocommerce_paypal_payments_seller_status', $fallback );

$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $status, self::SELLER_STATUS_CACHE_TTL );
Comment thread
danieldudzic marked this conversation as resolved.
Outdated

return $status;
}

throw $exception;
}

/** This filter is documented above. */
$status = apply_filters( 'woocommerce_paypal_payments_seller_status', $status );

$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $status, self::SELLER_STATUS_CACHE_TTL );
Comment thread
danieldudzic marked this conversation as resolved.
Outdated

return $status;
}

/**
* Fetches the seller status from the PayPal API.
*
* @return SellerStatus
* @throws RuntimeException When request could not be fulfilled.
*/
private function fetch_seller_status_from_api(): SellerStatus {
$url = trailingslashit( $this->host ) . 'v1/customer/partners/' . $this->partner_id . '/merchant-integrations/' . $this->merchant_id;
$bearer = $this->bearer->bearer();
$args = array(
Expand Down Expand Up @@ -189,11 +240,7 @@ public function seller_status(): SellerStatus {

$this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );

$status = $this->seller_status_factory->from_paypal_response( $json );

$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $status, self::SELLER_STATUS_CACHE_TTL );

return $status;
return $this->seller_status_factory->from_paypal_response( $json );
}

/**
Expand Down
56 changes: 56 additions & 0 deletions tests/PHPUnit/ApiClient/Endpoint/PartnersEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ public function testSellerStatusReturnsCachedValueWithoutHttpCall(): void
->with(PartnersEndpoint::SELLER_STATUS_CACHE_KEY)
->andReturn($cached_status);

when('apply_filters')->alias(function (string $hook, ...$args) {
return $args[0] ?? null;
});

// wp_remote_get must not be called — any invocation will cause the
// Brain Monkey expectation to fail with an unexpected call.
expect('wp_remote_get')->never();
Expand Down Expand Up @@ -211,6 +215,58 @@ public function testSellerStatusOnApiErrorRegistersFailureAndThrows(): void
$this->make_endpoint()->seller_status();
}

// -----------------------------------------------------------------------
// Tests: seller_status() fallback filter
// -----------------------------------------------------------------------

/**
* GIVEN the transient cache is empty
* AND the PayPal API returns a non-200 status code
* AND the fallback filter returns a SellerStatus
* WHEN seller_status() is called
* THEN the fallback is used instead of throwing
* AND the result is stored in the cache
*/
public function testSellerStatusOnApiErrorUsesFallbackFilter(): void
{
$fallback = Mockery::mock(SellerStatus::class);
$raw_response = $this->make_raw_response(500);

$this->cache
->shouldReceive('get')
->once()
->with(PartnersEndpoint::SELLER_STATUS_CACHE_KEY)
->andReturn(false);

$this->cache
->shouldReceive('set')
->once()
->with(
PartnersEndpoint::SELLER_STATUS_CACHE_KEY,
$fallback,
PartnersEndpoint::SELLER_STATUS_CACHE_TTL
);

$this->failure_registry
->shouldReceive('add_failure')
->once()
->with(FailureRegistry::SELLER_STATUS_KEY);

when('apply_filters')->alias(function (string $hook, ...$args) use ($fallback) {
if ($hook === 'woocommerce_paypal_payments_seller_status_fallback') {
return $fallback;
}
return $args[0] ?? null;
});
when('wp_remote_get')->justReturn($raw_response);
when('is_wp_error')->justReturn(false);
when('wp_remote_retrieve_response_code')->justReturn(500);

$result = $this->make_endpoint()->seller_status();

$this->assertSame($fallback, $result);
}

// -----------------------------------------------------------------------
// Tests: clear_seller_status_cache()
// -----------------------------------------------------------------------
Expand Down
Loading