Skip to content

Commit 07babdb

Browse files
authored
Merge pull request #4307 from woocommerce/dev/PCP-6276-seller-status-override-to-access-acdc-vaulting-configuration
Add SellerStatus filtering layer with extension point for custom overrides (6276)
2 parents a96febb + e75f974 commit 07babdb

3 files changed

Lines changed: 140 additions & 6 deletions

File tree

docs/seller-status-filter.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Seller Status Filter
2+
3+
## Overview
4+
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.
5+
6+
## Filters
7+
8+
### `woocommerce_paypal_payments_seller_status`
9+
Filters the `SellerStatus` object on every return from `PartnersEndpoint::seller_status()` — whether from cache or a fresh API call.
10+
11+
```php
12+
add_filter( 'woocommerce_paypal_payments_seller_status', function ( SellerStatus $status ): SellerStatus {
13+
// Modify and return.
14+
return $status;
15+
} );
16+
```
17+
18+
### `woocommerce_paypal_payments_seller_status_fallback`
19+
Provides a fallback `SellerStatus` when the API call fails. Return a `SellerStatus` to use it; return `null` to let the exception propagate.
20+
21+
```php
22+
add_filter( 'woocommerce_paypal_payments_seller_status_fallback', function () {
23+
return new SellerStatus( array(), array(), 'US' );
24+
} );
25+
```
26+
27+
## Example: Enable ACDC + Vaulting on Stage
28+
```php
29+
add_filter( 'woocommerce_paypal_payments_seller_status_fallback', function () {
30+
return new SellerStatus( array(), array(), 'US' );
31+
} );
32+
33+
add_filter( 'woocommerce_paypal_payments_seller_status', function ( SellerStatus $status ): SellerStatus {
34+
// Build modified products/capabilities and return a new SellerStatus.
35+
} );
36+
```

modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,55 @@ public function __construct(
135135
public function seller_status(): SellerStatus {
136136
$cached = $this->cache->get( self::SELLER_STATUS_CACHE_KEY );
137137
if ( $cached instanceof SellerStatus ) {
138-
return $cached;
138+
/**
139+
* Filters the seller status object before it is returned.
140+
*
141+
* @param SellerStatus $status The seller status (from cache or API).
142+
*/
143+
return apply_filters( 'woocommerce_paypal_payments_seller_status', $cached );
139144
}
140145

146+
try {
147+
$status = $this->fetch_seller_status_from_api();
148+
} catch ( RuntimeException $exception ) {
149+
/**
150+
* Provides a fallback SellerStatus when the API call fails.
151+
*
152+
* Return a SellerStatus instance to use as fallback instead of
153+
* throwing. Return null to let the exception propagate.
154+
*
155+
* @param SellerStatus|null $fallback Default null (no fallback).
156+
*/
157+
$fallback = apply_filters( 'woocommerce_paypal_payments_seller_status_fallback', null );
158+
159+
if ( $fallback instanceof SellerStatus ) {
160+
$this->logger->log(
161+
'info',
162+
'Seller status API failed, using configured fallback.',
163+
array( 'error' => $exception->getMessage() )
164+
);
165+
166+
$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $fallback, self::SELLER_STATUS_CACHE_TTL );
167+
168+
return apply_filters( 'woocommerce_paypal_payments_seller_status', $fallback );
169+
}
170+
171+
throw $exception;
172+
}
173+
174+
$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $status, self::SELLER_STATUS_CACHE_TTL );
175+
176+
/** This filter is documented above. */
177+
return apply_filters( 'woocommerce_paypal_payments_seller_status', $status );
178+
}
179+
180+
/**
181+
* Fetches the seller status from the PayPal API.
182+
*
183+
* @return SellerStatus
184+
* @throws RuntimeException When request could not be fulfilled.
185+
*/
186+
private function fetch_seller_status_from_api(): SellerStatus {
141187
$url = trailingslashit( $this->host ) . 'v1/customer/partners/' . $this->partner_id . '/merchant-integrations/' . $this->merchant_id;
142188
$bearer = $this->bearer->bearer();
143189
$args = array(
@@ -189,11 +235,7 @@ public function seller_status(): SellerStatus {
189235

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

192-
$status = $this->seller_status_factory->from_paypal_response( $json );
193-
194-
$this->cache->set( self::SELLER_STATUS_CACHE_KEY, $status, self::SELLER_STATUS_CACHE_TTL );
195-
196-
return $status;
238+
return $this->seller_status_factory->from_paypal_response( $json );
197239
}
198240

199241
/**

tests/PHPUnit/ApiClient/Endpoint/PartnersEndpointTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ public function testSellerStatusReturnsCachedValueWithoutHttpCall(): void
124124
->with(PartnersEndpoint::SELLER_STATUS_CACHE_KEY)
125125
->andReturn($cached_status);
126126

127+
when('apply_filters')->alias(function (string $hook, ...$args) {
128+
return $args[0] ?? null;
129+
});
130+
127131
// wp_remote_get must not be called — any invocation will cause the
128132
// Brain Monkey expectation to fail with an unexpected call.
129133
expect('wp_remote_get')->never();
@@ -211,6 +215,58 @@ public function testSellerStatusOnApiErrorRegistersFailureAndThrows(): void
211215
$this->make_endpoint()->seller_status();
212216
}
213217

218+
// -----------------------------------------------------------------------
219+
// Tests: seller_status() fallback filter
220+
// -----------------------------------------------------------------------
221+
222+
/**
223+
* GIVEN the transient cache is empty
224+
* AND the PayPal API returns a non-200 status code
225+
* AND the fallback filter returns a SellerStatus
226+
* WHEN seller_status() is called
227+
* THEN the fallback is used instead of throwing
228+
* AND the result is stored in the cache
229+
*/
230+
public function testSellerStatusOnApiErrorUsesFallbackFilter(): void
231+
{
232+
$fallback = Mockery::mock(SellerStatus::class);
233+
$raw_response = $this->make_raw_response(500);
234+
235+
$this->cache
236+
->shouldReceive('get')
237+
->once()
238+
->with(PartnersEndpoint::SELLER_STATUS_CACHE_KEY)
239+
->andReturn(false);
240+
241+
$this->cache
242+
->shouldReceive('set')
243+
->once()
244+
->with(
245+
PartnersEndpoint::SELLER_STATUS_CACHE_KEY,
246+
$fallback,
247+
PartnersEndpoint::SELLER_STATUS_CACHE_TTL
248+
);
249+
250+
$this->failure_registry
251+
->shouldReceive('add_failure')
252+
->once()
253+
->with(FailureRegistry::SELLER_STATUS_KEY);
254+
255+
when('apply_filters')->alias(function (string $hook, ...$args) use ($fallback) {
256+
if ($hook === 'woocommerce_paypal_payments_seller_status_fallback') {
257+
return $fallback;
258+
}
259+
return $args[0] ?? null;
260+
});
261+
when('wp_remote_get')->justReturn($raw_response);
262+
when('is_wp_error')->justReturn(false);
263+
when('wp_remote_retrieve_response_code')->justReturn(500);
264+
265+
$result = $this->make_endpoint()->seller_status();
266+
267+
$this->assertSame($fallback, $result);
268+
}
269+
214270
// -----------------------------------------------------------------------
215271
// Tests: clear_seller_status_cache()
216272
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)