Merge pull request #3251 from woocommerce/PCP-4391-php-filter-bn-code

Introduce the `PartnerAttribution` helper for handling the BN codes (4391)
This commit is contained in:
Philipp Stracker 2025-03-21 16:47:45 +01:00 committed by GitHub
commit 1ba71b5584
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 302 additions and 18 deletions

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
@ -31,6 +32,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\ActivationDetector;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
@ -963,4 +965,14 @@ return array(
return CONNECT_WOO_URL;
},
'api.helper.partner-attribution' => static function ( ContainerInterface $container ) : PartnerAttribution {
return new PartnerAttribution(
'ppcp_bn_code',
array(
ActivationDetector::CORE_PROFILER => 'WooPPCP_Ecom_PS_CoreProfiler',
ActivationDetector::PAYMENT_SETTINGS => 'WooPPCP_Ecom_PS_CoreProfiler',
),
PPCP_PAYPAL_BN_CODE
);
},
);

View file

@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -133,6 +134,38 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
/**
* Filters the request arguments to add the `'PayPal-Partner-Attribution-Id'` header.
*
* This ensures that all API requests include the appropriate BN code retrieved
* from the `PartnerAttribution` helper. Using this approach avoids the need
* for extensive refactoring of existing classes that use the `RequestTrait`.
*
* The filter is applied in {@see RequestTrait::request()} before making an API request.
*
* @see PartnerAttribution::get_bn_code() Retrieves the BN code dynamically.
* @see RequestTrait::request() Where the filter `ppcp_request_args` is applied.
*/
add_filter(
'ppcp_request_args',
static function ( array $args ) use ( $c ) {
if ( isset( $args['headers']['PayPal-Partner-Attribution-Id'] ) ) {
return $args;
}
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
if ( ! isset( $args['headers'] ) || ! is_array( $args['headers'] ) ) {
$args['headers'] = array();
}
$args['headers']['PayPal-Partner-Attribution-Id'] = $partner_attribution->get_bn_code();
return $args;
}
);
return true;
}
}

View file

@ -41,9 +41,6 @@ trait RequestTrait {
* added here.
*/
$args = apply_filters( 'ppcp_request_args', $args, $url );
if ( ! isset( $args['headers']['PayPal-Partner-Attribution-Id'] ) ) {
$args['headers']['PayPal-Partner-Attribution-Id'] = PPCP_PAYPAL_BN_CODE;
}
$response = wp_remote_get( $url, $args );
if ( $this->is_request_logging_enabled ) {

View file

@ -0,0 +1,91 @@
<?php
/**
* PayPal Partner Attribution Helper.
*
* This class handles the retrieval and persistence of the BN (Build Notation) Code,
* which is used to track and attribute transactions for PayPal partner integrations.
*
* The BN Code is set once and remains persistent, even after disconnecting
* or uninstalling the plugin. It is determined based on the installation path
* and stored as a WordPress option.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* PayPal Partner Attribution Helper.
*
* @psalm-type installationPath = string
* @psalm-type bnCode = string
*/
class PartnerAttribution {
/**
* The BN code option name in DB.
*
* @var string
*/
protected string $bn_code_option_name;
/**
* BN Codes mapping for different installation paths.
*
* @var array<installationPath, bnCode>
*/
protected array $bn_codes;
/**
* The default BN code.
*
* @var string
*/
protected string $default_bn_code;
/**
* PartnerAttribution constructor.
*
* @param string $bn_code_option_name The BN code option name in DB.
* @param array<installationPath, bnCode> $bn_codes BN Codes mapping for different installation paths.
* @param string $default_bn_code The default BN code.
*/
public function __construct( string $bn_code_option_name, array $bn_codes, string $default_bn_code ) {
$this->bn_code_option_name = $bn_code_option_name;
$this->bn_codes = $bn_codes;
$this->default_bn_code = $default_bn_code;
}
/**
* Initializes the BN Code if not already set.
*
* This method ensures that the BN Code is only stored once during the initial setup.
*
* @param string $installation_path The installation path used to determine the BN Code.
*/
public function initialize_bn_code( string $installation_path ): void {
$selected_bn_code = $this->bn_codes[ $installation_path ] ?? false;
if ( get_option( $this->bn_code_option_name ) || ! $selected_bn_code ) {
return;
}
update_option( $this->bn_code_option_name, $selected_bn_code );
}
/**
* Retrieves the persisted BN Code.
*
* @return string The stored BN Code, or the default value if no path is detected.
*/
public function get_bn_code(): string {
$bn_code = get_option( $this->bn_code_option_name, $this->default_bn_code ) ?? $this->default_bn_code;
if ( ! in_array( $bn_code, $this->bn_codes, true ) && $bn_code !== $this->default_bn_code ) {
return $this->default_bn_code;
}
return $bn_code;
}
}

View file

@ -168,7 +168,8 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$container->get( 'button.helper.disabled-funding-sources' ),
$container->get( 'wcgateway.configuration.card-configuration' )
$container->get( 'wcgateway.configuration.card-configuration' ),
$container->get( 'api.helper.partner-attribution' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
@ -245,6 +246,13 @@ class SmartButton implements SmartButtonInterface {
*/
private CardPaymentsConfiguration $dcc_configuration;
/**
* The PayPal Partner Attribution Helper.
*
* @var PartnerAttribution
*/
protected PartnerAttribution $partner_attribution;
/**
* SmartButton constructor.
*
@ -273,6 +281,7 @@ class SmartButton implements SmartButtonInterface {
* @param bool $should_handle_shipping_in_paypal Whether the shipping should be handled in PayPal.
* @param DisabledFundingSources $disabled_funding_sources List of funding sources to be disabled.
* @param CardPaymentsConfiguration $dcc_configuration The DCC Gateway Configuration.
* @param PartnerAttribution $partner_attribution The PayPal Partner Attribution Helper.
*/
public function __construct(
string $module_url,
@ -299,7 +308,8 @@ class SmartButton implements SmartButtonInterface {
LoggerInterface $logger,
bool $should_handle_shipping_in_paypal,
DisabledFundingSources $disabled_funding_sources,
CardPaymentsConfiguration $dcc_configuration
CardPaymentsConfiguration $dcc_configuration,
PartnerAttribution $partner_attribution
) {
$this->module_url = $module_url;
$this->version = $version;
@ -326,6 +336,7 @@ class SmartButton implements SmartButtonInterface {
$this->should_handle_shipping_in_paypal = $should_handle_shipping_in_paypal;
$this->disabled_funding_sources = $disabled_funding_sources;
$this->dcc_configuration = $dcc_configuration;
$this->partner_attribution = $partner_attribution;
}
/**
@ -841,9 +852,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
*/
do_action( "ppcp_before_{$location_hook}_message_wrapper" );
$bn_code = PPCP_PAYPAL_BN_CODE;
$messages_placeholder = '<div class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';
$messages_placeholder = '<div class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $this->partner_attribution->get_bn_code() ) . '"></div>';
if ( is_array( $block_params ) && ( $block_params['blockName'] ?? false ) ) {
$this->render_after_block(
@ -1540,12 +1549,9 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
* @return string
*/
private function bn_code_for_context( string $context ): string {
$codes = $this->bn_codes();
$bn_code = PPCP_PAYPAL_BN_CODE;
return ( isset( $codes[ $context ] ) ) ? $codes[ $context ] : $bn_code;
return ( isset( $codes[ $context ] ) ) ? $codes[ $context ] : $this->partner_attribution->get_bn_code();
}
/**
@ -1555,7 +1561,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
*/
private function bn_codes() : array {
$bn_code = PPCP_PAYPAL_BN_CODE;
$bn_code = $this->partner_attribution->get_bn_code();
return array(
'checkout' => $bn_code,

View file

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterBlock;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
@ -28,7 +29,10 @@ class PayLaterBlockRenderer {
public function render( array $attributes, ContainerInterface $c ): string {
if ( PayLaterBlockModule::is_block_enabled( $c->get( 'wcgateway.settings.status' ) ) ) {
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$bn_code = $partner_attribution->get_bn_code();
$html = '<div id="' . esc_attr( $attributes['id'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
@ -126,7 +127,8 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
$config_factory = $c->get( 'paylater-configurator.factory.config' );
assert( $config_factory instanceof ConfigFactory );
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
wp_localize_script(
'ppcp-paylater-configurator',
@ -145,7 +147,7 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
'config' => $config_factory->from_settings( $settings ),
'merchantClientId' => $settings->get( 'client_id' ),
'partnerClientId' => $c->get( 'api.partner_merchant_id' ),
'bnCode' => $bn_code,
'bnCode' => $partner_attribution->get_bn_code(),
'publishButtonClassName' => 'ppcp-paylater-configurator-publishButton',
'headerClassName' => 'ppcp-paylater-configurator-header',
'subheaderClassName' => 'ppcp-paylater-configurator-subheader',

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
@ -95,7 +96,10 @@ class PayLaterWCBlocksRenderer {
) {
if ( PayLaterWCBlocksModule::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), $location ) ) {
$bn_code = PPCP_PAYPAL_BN_CODE;
$partner_attribution = $c->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$bn_code = $partner_attribution->get_bn_code();
$html = '<div id="' . esc_attr( $attributes['ppcpId'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="' . esc_attr( $bn_code ) . '"></div>';

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
@ -162,7 +163,14 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$path_repository = $container->get( 'settings.service.branded-experience.path-repository' );
assert( $path_repository instanceof PathRepository );
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
$general_settings = $container->get( 'settings.data.general' );
assert( $general_settings instanceof GeneralSettings );
$path_repository->persist();
$partner_attribution->initialize_bn_code( $general_settings->get_installation_path() );
}
);
@ -236,6 +244,9 @@ class SettingsModule implements ServiceModule, ExecutableModule {
);
if ( $is_pay_later_configurator_available ) {
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
assert( $partner_attribution instanceof PartnerAttribution );
wp_enqueue_script(
'ppcp-paylater-configurator-lib',
'https://www.paypalobjects.com/merchant-library/merchant-configurator.js',
@ -251,7 +262,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'config' => array(),
'merchantClientId' => $settings->get( 'client_id' ),
'partnerClientId' => $container->get( 'api.partner_merchant_id' ),
'bnCode' => PPCP_PAYPAL_BN_CODE,
'bnCode' => $partner_attribution->get_bn_code(),
);
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use PHPUnit\Framework\TestCase;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\ActivationDetector;
use function Brain\Monkey\Functions\when;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\setUp;
use function Brain\Monkey\tearDown;
/**
* Test case for PartnerAttribution class.
*/
class PartnerAttributionTest extends TestCase {
/**
* Option name for storing the BN Code.
*
* @var string
*/
private string $bn_code_option_name = 'ppcp_bn_code';
/**
* BN codes mapping.
*
* @var array
*/
private array $bn_codes = array(
ActivationDetector::CORE_PROFILER => 'WooPPCP_Ecom_PS_CoreProfiler',
ActivationDetector::PAYMENT_SETTINGS => 'WooPPCP_Ecom_PS_CoreProfiler',
);
/**
* The default BN code.
*
* @var string
*/
private string $default_bn_code = 'Woo_PPCP';
/**
* Set up Brain Monkey before each test.
*/
protected function setUp(): void {
parent::setUp();
setUp(); // ✅ REQUIRED for Brain Monkey to work
}
/**
* Tear down Brain Monkey after each test.
*/
protected function tearDown(): void {
tearDown(); // ✅ REQUIRED to reset mocks after each test
parent::tearDown();
}
/**
* Tests initializing BN code when it's not already set.
*/
public function test_initialize_bn_code_sets_bn_code_if_not_present(): void {
$installation_path = 'core-profiler';
$expected_bn_code = 'WooPPCP_Ecom_PS_CoreProfiler';
// Ensure get_option returns false to simulate "not set" state
when('get_option')->justReturn(false);
// Expect update_option to be called once with the correct values
expect('update_option')
->once()
->with($this->bn_code_option_name, $expected_bn_code)
->andReturn(true);
$partner_attribution = new PartnerAttribution( $this->bn_code_option_name, $this->bn_codes, $this->default_bn_code );
$partner_attribution->initialize_bn_code( $installation_path );
$this->assertTrue(true);
}
/**
* Tests that initialize_bn_code does nothing if the BN code is already set.
*/
public function test_initialize_bn_code_does_not_update_if_already_set(): void {
when('get_option')->justReturn('WooPPCP_Ecom_PS_CoreProfiler');
expect( 'update_option' )->never();
$partner_attribution = new PartnerAttribution( $this->bn_code_option_name, $this->bn_codes, $this->default_bn_code );
$partner_attribution->initialize_bn_code( 'core-profiler' );
$this->assertTrue(true);
}
/**
* Tests retrieving the BN Code.
*/
public function test_get_bn_code_returns_persisted_value(): void {
$expected_bn_code = 'WooPPCP_Ecom_PS_CoreProfiler';
expect( 'get_option' )
->once()
->with( $this->bn_code_option_name, $this->default_bn_code )
->andReturn( $expected_bn_code );
$partner_attribution = new PartnerAttribution( $this->bn_code_option_name, $this->bn_codes, $this->default_bn_code );
$this->assertSame( $expected_bn_code, $partner_attribution->get_bn_code() );
}
/**
* Tests that get_bn_code returns an empty string if no value is stored.
*/
public function test_get_bn_code_returns_empty_string_when_not_set(): void {
expect( 'get_option' )
->once()
->with( $this->bn_code_option_name, $this->default_bn_code )
->andReturn( $this->default_bn_code );
$partner_attribution = new PartnerAttribution( $this->bn_code_option_name, $this->bn_codes, $this->default_bn_code );
$this->assertSame( $this->default_bn_code, $partner_attribution->get_bn_code() );
}
}