diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index bfadb2cda..a06afe8dd 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -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 + ); + }, ); diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php index a2880761c..12cdd12a6 100644 --- a/modules/ppcp-api-client/src/ApiModule.php +++ b/modules/ppcp-api-client/src/ApiModule.php @@ -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; } } diff --git a/modules/ppcp-api-client/src/Endpoint/RequestTrait.php b/modules/ppcp-api-client/src/Endpoint/RequestTrait.php index cb5f3c543..6d7a8ee4b 100644 --- a/modules/ppcp-api-client/src/Endpoint/RequestTrait.php +++ b/modules/ppcp-api-client/src/Endpoint/RequestTrait.php @@ -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 ) { diff --git a/modules/ppcp-api-client/src/Helper/PartnerAttribution.php b/modules/ppcp-api-client/src/Helper/PartnerAttribution.php new file mode 100644 index 000000000..f83aeb292 --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/PartnerAttribution.php @@ -0,0 +1,91 @@ + + */ + 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 $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; + } +} diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index a11516b82..18d9e27e4 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -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 { diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 477f73cb8..5cd26fcbe 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -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 = '
'; + $messages_placeholder = '
'; 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, diff --git a/modules/ppcp-paylater-block/src/PayLaterBlockRenderer.php b/modules/ppcp-paylater-block/src/PayLaterBlockRenderer.php index 74e8586b3..2408ba091 100644 --- a/modules/ppcp-paylater-block/src/PayLaterBlockRenderer.php +++ b/modules/ppcp-paylater-block/src/PayLaterBlockRenderer.php @@ -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 = '
'; diff --git a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index 71f33e1da..b877d9545 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -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', diff --git a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksRenderer.php b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksRenderer.php index f0720300b..8292f312f 100644 --- a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksRenderer.php +++ b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksRenderer.php @@ -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 = '
'; diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 9be213423..1a9b5b08f 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -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(), ); } diff --git a/tests/PHPUnit/ApiClient/Helper/PartnerAttributionTest.php b/tests/PHPUnit/ApiClient/Helper/PartnerAttributionTest.php new file mode 100644 index 000000000..bf99e29e0 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Helper/PartnerAttributionTest.php @@ -0,0 +1,123 @@ + '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() ); + } +}