mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Merge trunk
This commit is contained in:
commit
4a20d6a00c
40 changed files with 1458 additions and 305 deletions
|
@ -682,6 +682,11 @@ return array(
|
|||
'GB' => $default_currencies,
|
||||
'US' => $default_currencies,
|
||||
'NO' => $default_currencies,
|
||||
'YT' => $default_currencies,
|
||||
'RE' => $default_currencies,
|
||||
'GP' => $default_currencies,
|
||||
'GF' => $default_currencies,
|
||||
'MQ' => $default_currencies,
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -135,12 +135,12 @@ class BillingAgreementsEndpoint {
|
|||
);
|
||||
} finally {
|
||||
$this->is_request_logging_enabled = true;
|
||||
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
|
||||
}
|
||||
|
||||
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
|
||||
return true;
|
||||
} catch ( Exception $exception ) {
|
||||
delete_transient( 'ppcp_reference_transaction_enabled' );
|
||||
set_transient( 'ppcp_reference_transaction_enabled', false, HOUR_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,20 +58,22 @@ class PartnerAttribution {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initializes the BN Code if not already set.
|
||||
*
|
||||
* This method ensures that the BN Code is only stored once during the initial setup.
|
||||
* Initializes or updates the BN Code.
|
||||
*
|
||||
* @param string $installation_path The installation path used to determine the BN Code.
|
||||
* @param bool $force_update Whether to force an update of the BN code if it already exists.
|
||||
*/
|
||||
public function initialize_bn_code( string $installation_path ) : void {
|
||||
public function initialize_bn_code( string $installation_path, bool $force_update = false ) : void {
|
||||
$selected_bn_code = $this->bn_codes[ $installation_path ] ?? '';
|
||||
|
||||
if ( ! $selected_bn_code || get_option( $this->bn_code_option_name ) ) {
|
||||
if ( ! $selected_bn_code ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing_bn_code = get_option( $this->bn_code_option_name );
|
||||
if ( $existing_bn_code && ! $force_update ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This option is permanent and should not change.
|
||||
update_option( $this->bn_code_option_name, $selected_bn_code );
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Applepay;
|
|||
|
||||
use WC_Payment_Gateway;
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
|
||||
use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton;
|
||||
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
|
||||
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
|
||||
|
@ -198,6 +199,31 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
}
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'ppcp_create_order_request_body_data',
|
||||
static function ( array $data, string $payment_method, array $request ) use ( $c ) : array {
|
||||
|
||||
if ( $payment_method !== ApplePayGateway::ID ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
|
||||
assert( $experience_context_builder instanceof ExperienceContextBuilder );
|
||||
|
||||
$data['payment_source'] = array(
|
||||
'apple_pay' => array(
|
||||
'experience_context' => $experience_context_builder
|
||||
->with_endpoint_return_urls()
|
||||
->build()->to_array(),
|
||||
),
|
||||
);
|
||||
|
||||
return $data;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -392,50 +392,6 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the checkout has been validated we execute this method.
|
||||
*
|
||||
* @param array $data The data.
|
||||
* @param \WP_Error $errors The errors, which occurred.
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception On Error.
|
||||
*/
|
||||
public function after_checkout_validation( array $data, \WP_Error $errors ): array {
|
||||
if ( ! $errors->errors ) {
|
||||
try {
|
||||
$order = $this->create_paypal_order();
|
||||
} catch ( Exception $exception ) {
|
||||
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* In case we are onboarded and everything is fine with the \WC_Order
|
||||
* we want this order to be created. We will intercept it and leave it
|
||||
* in the "Pending payment" status though, which than later will change
|
||||
* during the "onApprove"-JS callback or the webhook listener.
|
||||
*/
|
||||
if ( ! $this->early_order_handler->should_create_early_order() ) {
|
||||
wp_send_json_success( $this->make_response( $order ) );
|
||||
}
|
||||
$this->early_order_handler->register_for_order( $order );
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->logger->error( 'Checkout validation failed: ' . $errors->get_error_message() );
|
||||
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => '',
|
||||
'message' => $errors->get_error_message(),
|
||||
'code' => (int) $errors->get_error_code(),
|
||||
'details' => array(),
|
||||
)
|
||||
);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the order in the PayPal, uses data from WC order if provided.
|
||||
*
|
||||
|
@ -485,8 +441,13 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
}
|
||||
}
|
||||
|
||||
$payment_source_key = 'paypal';
|
||||
if ( in_array( $funding_source, array( 'venmo' ), true ) ) {
|
||||
$payment_source_key = $funding_source;
|
||||
}
|
||||
|
||||
$payment_source = new PaymentSource(
|
||||
'paypal',
|
||||
$payment_source_key,
|
||||
(object) array(
|
||||
'experience_context' => $this->experience_context_builder
|
||||
->with_default_paypal_config( $shipping_preference, $action )
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\CardFields;
|
|||
use DomainException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
|
||||
use WooCommerce\PayPalCommerce\CardFields\Service\CardCaptureValidator;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
|
||||
|
@ -150,6 +151,15 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu
|
|||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
|
||||
assert( $experience_context_builder instanceof ExperienceContextBuilder );
|
||||
|
||||
$payment_source_data = array(
|
||||
'experience_context' => $experience_context_builder
|
||||
->with_endpoint_return_urls()
|
||||
->build()->to_array(),
|
||||
);
|
||||
|
||||
$three_d_secure_contingency =
|
||||
$settings->has( '3d_secure_contingency' )
|
||||
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
|
||||
|
@ -159,15 +169,15 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu
|
|||
$three_d_secure_contingency === 'SCA_ALWAYS'
|
||||
|| $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
|
||||
) {
|
||||
$data['payment_source']['card'] = array(
|
||||
'attributes' => array(
|
||||
'verification' => array(
|
||||
'method' => $three_d_secure_contingency,
|
||||
),
|
||||
$payment_source_data['attributes'] = array(
|
||||
'verification' => array(
|
||||
'method' => $three_d_secure_contingency,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$data['payment_source'] = array( 'card' => $payment_source_data );
|
||||
|
||||
return $data;
|
||||
},
|
||||
10,
|
||||
|
|
|
@ -71,6 +71,7 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
|
||||
$this->migrate_pay_later_settings( $c );
|
||||
$this->migrate_smart_button_settings( $c );
|
||||
$this->migrate_three_d_secure_setting();
|
||||
|
||||
$this->fix_page_builders();
|
||||
$this->exclude_cache_plugins_js_minification( $c );
|
||||
|
@ -274,6 +275,35 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrates the old Three D Secure setting located in PaymentSettings to the new location in SettingsModel.
|
||||
*
|
||||
* The migration will be done on plugin update if it hasn't already done.
|
||||
*/
|
||||
protected function migrate_three_d_secure_setting(): void {
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_gateway_migrate_on_update',
|
||||
function () {
|
||||
$payment_settings = get_option( 'woocommerce-ppcp-data-payment' ) ?: array();
|
||||
$data_settings = get_option( 'woocommerce-ppcp-data-settings' ) ?: array();
|
||||
|
||||
// Skip if payment settings don't have the setting but data settings do.
|
||||
if ( ! isset( $payment_settings['three_d_secure'] ) && isset( $data_settings['three_d_secure'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the setting.
|
||||
$data_settings['three_d_secure'] = $payment_settings['three_d_secure'];
|
||||
unset( $payment_settings['three_d_secure'] );
|
||||
|
||||
// Save both.
|
||||
update_option( 'woocommerce-ppcp-data-settings', $data_settings );
|
||||
update_option( 'woocommerce-ppcp-data-payment', $payment_settings );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the button rendering place for page builders
|
||||
* that do not work well with our default places.
|
||||
|
|
|
@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Compat\Settings;
|
|||
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
||||
/**
|
||||
|
@ -22,15 +21,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
|||
*/
|
||||
class PaymentMethodSettingsMapHelper {
|
||||
|
||||
/**
|
||||
* A map of new to old 3d secure values.
|
||||
*/
|
||||
protected const THREE_D_SECURE_VALUES_MAP = array(
|
||||
'no-3d-secure' => 'NO_3D_SECURE',
|
||||
'only-required-3d-secure' => 'SCA_WHEN_REQUIRED',
|
||||
'always-3d-secure' => 'SCA_ALWAYS',
|
||||
);
|
||||
|
||||
/**
|
||||
* Maps old setting keys to new payment method settings names.
|
||||
*
|
||||
|
@ -38,9 +28,8 @@ class PaymentMethodSettingsMapHelper {
|
|||
*/
|
||||
public function map(): array {
|
||||
return array(
|
||||
'dcc_enabled' => CreditCardGateway::ID,
|
||||
'axo_enabled' => AxoGateway::ID,
|
||||
'3d_secure_contingency' => 'three_d_secure',
|
||||
'dcc_enabled' => CreditCardGateway::ID,
|
||||
'axo_enabled' => AxoGateway::ID,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -52,25 +41,13 @@ class PaymentMethodSettingsMapHelper {
|
|||
* @return mixed The value of the mapped setting, (null if not found).
|
||||
*/
|
||||
public function mapped_value( string $old_key, ?AbstractDataModel $payment_settings ) {
|
||||
switch ( $old_key ) {
|
||||
case '3d_secure_contingency':
|
||||
if ( is_null( $payment_settings ) ) {
|
||||
return null;
|
||||
}
|
||||
$payment_method = $this->map()[ $old_key ] ?? false;
|
||||
|
||||
assert( $payment_settings instanceof PaymentSettings );
|
||||
$selected_three_d_secure = $payment_settings->get_three_d_secure();
|
||||
return self::THREE_D_SECURE_VALUES_MAP[ $selected_three_d_secure ] ?? null;
|
||||
|
||||
default:
|
||||
$payment_method = $this->map()[ $old_key ] ?? false;
|
||||
|
||||
if ( ! $payment_method ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->is_gateway_enabled( $payment_method );
|
||||
if ( ! $payment_method ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->is_gateway_enabled( $payment_method );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,17 @@ class SettingsTabMapHelper {
|
|||
|
||||
use ContextTrait;
|
||||
|
||||
/**
|
||||
* A map of new to old 3d secure values.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected const THREE_D_SECURE_VALUES_MAP = array(
|
||||
'no-3d-secure' => 'NO_3D_SECURE',
|
||||
'only-required-3d-secure' => 'SCA_WHEN_REQUIRED',
|
||||
'always-3d-secure' => 'SCA_ALWAYS',
|
||||
);
|
||||
|
||||
/**
|
||||
* Maps old setting keys to new setting keys.
|
||||
*
|
||||
|
@ -43,6 +54,7 @@ class SettingsTabMapHelper {
|
|||
'blocks_final_review_enabled' => 'enable_pay_now',
|
||||
'logging_enabled' => 'enable_logging',
|
||||
'vault_enabled' => 'save_paypal_and_venmo',
|
||||
'3d_secure_contingency' => 'threeDSecure',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -69,11 +81,30 @@ class SettingsTabMapHelper {
|
|||
case 'blocks_final_review_enabled':
|
||||
return $this->mapped_pay_now_value( $settings_model );
|
||||
|
||||
case '3d_secure_contingency':
|
||||
return $this->mapped_3d_secure_value( $settings_model );
|
||||
|
||||
default:
|
||||
return $settings_model[ $new_key ] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mapped value for the '3d_secure_contingency' from the new settings.
|
||||
*
|
||||
* @param array $settings_model The new settings model data.
|
||||
* @return string|null The mapped '3d_secure_contingency' setting value.
|
||||
*/
|
||||
protected function mapped_3d_secure_value( array $settings_model ): ?string {
|
||||
$three_d_secure = $settings_model['threeDSecure'] ?? null;
|
||||
|
||||
if ( ! is_string( $three_d_secure ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::THREE_D_SECURE_VALUES_MAP[ $three_d_secure ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mapped value for the 'mismatch_behavior' from the new settings.
|
||||
*
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay;
|
|||
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
|
||||
use WC_Payment_Gateway;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
|
||||
|
@ -261,6 +262,15 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
|
||||
assert( $experience_context_builder instanceof ExperienceContextBuilder );
|
||||
|
||||
$payment_source_data = array(
|
||||
'experience_context' => $experience_context_builder
|
||||
->with_endpoint_return_urls()
|
||||
->build()->to_array(),
|
||||
);
|
||||
|
||||
$three_d_secure_contingency =
|
||||
$settings->has( '3d_secure_contingency' )
|
||||
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
|
||||
|
@ -270,15 +280,15 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
$three_d_secure_contingency === 'SCA_ALWAYS'
|
||||
|| $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
|
||||
) {
|
||||
$data['payment_source']['google_pay'] = array(
|
||||
'attributes' => array(
|
||||
'verification' => array(
|
||||
'method' => $three_d_secure_contingency,
|
||||
),
|
||||
$payment_source_data['attributes'] = array(
|
||||
'verification' => array(
|
||||
'method' => $three_d_secure_contingency,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$data['payment_source'] = array( 'google_pay' => $payment_source_data );
|
||||
|
||||
return $data;
|
||||
},
|
||||
10,
|
||||
|
|
|
@ -78,6 +78,11 @@ return array(
|
|||
'SE',
|
||||
'GB',
|
||||
'US',
|
||||
'YT',
|
||||
'RE',
|
||||
'GP',
|
||||
'GF',
|
||||
'MQ',
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -115,87 +115,67 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
function ( array $data, string $payment_method, array $request_data ) use ( $c ): array {
|
||||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
$new_attributes = array(
|
||||
'vault' => array(
|
||||
'store_in_vault' => 'ON_SUCCESS',
|
||||
),
|
||||
);
|
||||
|
||||
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
|
||||
if ( ! $target_customer_id ) {
|
||||
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
|
||||
}
|
||||
if ( $target_customer_id ) {
|
||||
$new_attributes['customer'] = array(
|
||||
'id' => $target_customer_id,
|
||||
);
|
||||
}
|
||||
|
||||
$funding_source = (string) ( $request_data['funding_source'] ?? '' );
|
||||
|
||||
if ( $payment_method === CreditCardGateway::ID ) {
|
||||
if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$save_payment_method = $request_data['save_payment_method'] ?? false;
|
||||
if ( $save_payment_method ) {
|
||||
$data['payment_source'] = array(
|
||||
'card' => array(
|
||||
'attributes' => array(
|
||||
'vault' => array(
|
||||
'store_in_vault' => 'ON_SUCCESS',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
|
||||
if ( ! $target_customer_id ) {
|
||||
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
|
||||
}
|
||||
|
||||
if ( $target_customer_id ) {
|
||||
$data['payment_source']['card']['attributes']['customer'] = array(
|
||||
'id' => $target_customer_id,
|
||||
);
|
||||
}
|
||||
if ( ! $save_payment_method ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $payment_method === PayPalGateway::ID ) {
|
||||
} elseif ( $payment_method === PayPalGateway::ID ) {
|
||||
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$funding_source = $request_data['funding_source'] ?? null;
|
||||
|
||||
if ( $funding_source && $funding_source === 'venmo' ) {
|
||||
$data['payment_source'] = array(
|
||||
'venmo' => array(
|
||||
'attributes' => array(
|
||||
'vault' => array(
|
||||
'store_in_vault' => 'ON_SUCCESS',
|
||||
'usage_type' => 'MERCHANT',
|
||||
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} elseif ( $funding_source && $funding_source === 'apple_pay' ) {
|
||||
$data['payment_source'] = array(
|
||||
'apple_pay' => array(
|
||||
'stored_credential' => array(
|
||||
'payment_initiator' => 'CUSTOMER',
|
||||
'payment_type' => 'RECURRING',
|
||||
),
|
||||
'attributes' => array(
|
||||
'vault' => array(
|
||||
'store_in_vault' => 'ON_SUCCESS',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
$data['payment_source'] = array(
|
||||
'paypal' => array(
|
||||
'attributes' => array(
|
||||
'vault' => array(
|
||||
'store_in_vault' => 'ON_SUCCESS',
|
||||
'usage_type' => 'MERCHANT',
|
||||
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if ( ! in_array( $funding_source, array( 'paypal', 'venmo' ), true ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$new_attributes['vault']['usage_type'] = 'MERCHANT';
|
||||
$new_attributes['vault']['permit_multiple_payment_tokens'] = apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false );
|
||||
} else {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$payment_source = (array) ( $data['payment_source'] ?? array() );
|
||||
$key = array_key_first( $payment_source );
|
||||
if ( ! is_string( $key ) || empty( $key ) ) {
|
||||
$key = $payment_method;
|
||||
if ( $payment_method === PayPalGateway::ID && $funding_source ) {
|
||||
$key = $funding_source;
|
||||
}
|
||||
$payment_source[ $key ] = array();
|
||||
}
|
||||
$payment_source[ $key ] = (array) $payment_source[ $key ];
|
||||
$attributes = (array) ( $payment_source[ $key ]['attributes'] ?? array() );
|
||||
$payment_source[ $key ]['attributes'] = array_merge( $attributes, $new_attributes );
|
||||
|
||||
$data['payment_source'] = $payment_source;
|
||||
|
||||
return $data;
|
||||
},
|
||||
10,
|
||||
20,
|
||||
3
|
||||
);
|
||||
|
||||
|
|
|
@ -69,3 +69,23 @@
|
|||
margin-top: var(--block-action-gap, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp--notice {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
line-height: 1.5714285714;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--notice-background);
|
||||
color: var(--notice-text);
|
||||
|
||||
&.type--info {
|
||||
--notice-background: var(--color-success-background);
|
||||
--notice-text: var(--color-success-text);
|
||||
}
|
||||
|
||||
&.type--error {
|
||||
--notice-background: var(--color-failure-background);
|
||||
--notice-text: var(--color-failure-text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
|
||||
.ppcp-r-inner-container {
|
||||
max-width: var(--max-width-onboarding-content);
|
||||
|
||||
&.ppcp--wide {
|
||||
--max-width-onboarding-content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r-payment-method--separator {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
const Notice = ( { children, type = 'info', className = '' } ) => {
|
||||
if ( ! children ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elementClasses = classNames(
|
||||
'ppcp--notice',
|
||||
`type--${ type }`,
|
||||
className
|
||||
);
|
||||
|
||||
return <span className={ elementClasses }>{ children }</span>;
|
||||
};
|
||||
|
||||
export default Notice;
|
|
@ -9,6 +9,7 @@ export { default as ContentWrapper } from './ContentWrapper';
|
|||
export { default as Description } from './Description';
|
||||
export { default as Header } from './Header';
|
||||
export { default as LearnMore } from './LearnMore';
|
||||
export { default as Notice } from './Notice';
|
||||
export { default as Separator } from './Separator';
|
||||
export { default as Title } from './Title';
|
||||
export { default as TitleExtra } from './TitleExtra';
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import { Button } from '@wordpress/components';
|
||||
import { useEffect, useCallback } from '@wordpress/element';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { OpenSignup } from '../../../ReusableComponents/Icons';
|
||||
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
|
||||
import { OnboardingHooks } from '../../../../data/onboarding/hooks';
|
||||
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||
import { Notice } from '../../../ReusableComponents/Elements';
|
||||
|
||||
const useIsFirefox = () => {
|
||||
if ( typeof window === 'undefined' ) {
|
||||
return false;
|
||||
}
|
||||
return window.navigator.userAgent.toLowerCase().indexOf( 'firefox' ) > -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Button component that outputs a placeholder button when no onboardingUrl is present yet - the
|
||||
|
@ -27,6 +36,8 @@ const ButtonOrPlaceholder = ( {
|
|||
children,
|
||||
onClick,
|
||||
} ) => {
|
||||
const isFirefox = useIsFirefox();
|
||||
|
||||
const buttonProps = {
|
||||
className,
|
||||
variant,
|
||||
|
@ -40,6 +51,20 @@ const ButtonOrPlaceholder = ( {
|
|||
buttonProps[ 'data-paypal-onboard-button' ] = 'true';
|
||||
}
|
||||
|
||||
if ( isFirefox ) {
|
||||
return (
|
||||
<>
|
||||
<Button { ...buttonProps }>{ children }</Button>
|
||||
<Notice type={ 'error' }>
|
||||
{ __(
|
||||
'This button may not work in Firefox. Please use another browser, like Chrome, to complete this step.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Notice>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Button { ...buttonProps }>{ children }</Button>;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const StepCompleteSetup = () => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<div className="ppcp-r-inner-container ppcp--wide">
|
||||
<div className="ppcp-r-onboarding-header__description">
|
||||
<ConnectionButton
|
||||
title={ __(
|
||||
|
|
|
@ -12,12 +12,8 @@ import { PaymentHooks } from '../../../../../data';
|
|||
|
||||
const Modal = ( { method, setModalIsVisible, onSave } ) => {
|
||||
const { all: paymentMethods } = PaymentHooks.usePaymentMethods();
|
||||
const {
|
||||
paypalShowLogo,
|
||||
threeDSecure,
|
||||
fastlaneCardholderName,
|
||||
fastlaneDisplayWatermark,
|
||||
} = PaymentHooks.usePaymentMethodsModal();
|
||||
const { paypalShowLogo, fastlaneCardholderName, fastlaneDisplayWatermark } =
|
||||
PaymentHooks.usePaymentMethodsModal();
|
||||
|
||||
const [ settings, setSettings ] = useState( () => {
|
||||
if ( ! method?.id ) {
|
||||
|
@ -44,7 +40,6 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
|
|||
} );
|
||||
|
||||
initialSettings.paypalShowLogo = paypalShowLogo;
|
||||
initialSettings.threeDSecure = threeDSecure;
|
||||
initialSettings.fastlaneCardholderName = fastlaneCardholderName;
|
||||
initialSettings.fastlaneDisplayWatermark = fastlaneDisplayWatermark;
|
||||
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import Accordion from '../../../../../ReusableComponents/AccordionSection';
|
||||
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
|
||||
import { ControlSelect } from '../../../../../ReusableComponents/Controls';
|
||||
import {
|
||||
ControlSelect,
|
||||
ControlRadioGroup,
|
||||
} from '../../../../../ReusableComponents/Controls';
|
||||
import { SettingsHooks } from '../../../../../../data';
|
||||
|
||||
const OtherSettings = () => {
|
||||
const { disabledCards, setDisabledCards } = SettingsHooks.useSettings();
|
||||
const { disabledCards, setDisabledCards, threeDSecure, setThreeDSecure } =
|
||||
SettingsHooks.useSettings();
|
||||
|
||||
const disabledCardChoices = window.ppcpSettings.disabledCardsChoices;
|
||||
const threeDSecureOptions = window.ppcpSettings.threeDSecureOptions;
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
title={ __(
|
||||
|
@ -40,6 +46,19 @@ const OtherSettings = () => {
|
|||
) }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
<SettingsBlock
|
||||
title={ __( '3D Secure', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
>
|
||||
<ControlRadioGroup
|
||||
options={ threeDSecureOptions }
|
||||
value={ threeDSecure }
|
||||
onChange={ setThreeDSecure }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -129,7 +129,6 @@ export const usePaymentMethodsModal = () => {
|
|||
const { usePersistent } = useStoreData();
|
||||
|
||||
const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
|
||||
const [ threeDSecure ] = usePersistent( 'threeDSecure' );
|
||||
const [ fastlaneCardholderName ] = usePersistent(
|
||||
'fastlaneCardholderName'
|
||||
);
|
||||
|
@ -139,7 +138,6 @@ export const usePaymentMethodsModal = () => {
|
|||
|
||||
return {
|
||||
paypalShowLogo,
|
||||
threeDSecure,
|
||||
fastlaneCardholderName,
|
||||
fastlaneDisplayWatermark,
|
||||
};
|
||||
|
|
|
@ -68,6 +68,8 @@ const useHooks = () => {
|
|||
const [ disabledCards, setDisabledCards ] =
|
||||
usePersistent( 'disabledCards' );
|
||||
|
||||
const [ threeDSecure, setThreeDSecure ] = usePersistent( 'threeDSecure' );
|
||||
|
||||
return {
|
||||
invoicePrefix,
|
||||
setInvoicePrefix,
|
||||
|
@ -97,6 +99,8 @@ const useHooks = () => {
|
|||
setButtonLanguage,
|
||||
disabledCards,
|
||||
setDisabledCards,
|
||||
threeDSecure,
|
||||
setThreeDSecure,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -143,6 +147,8 @@ export const useSettings = () => {
|
|||
setButtonLanguage,
|
||||
disabledCards,
|
||||
setDisabledCards,
|
||||
threeDSecure,
|
||||
setThreeDSecure,
|
||||
} = useHooks();
|
||||
|
||||
return {
|
||||
|
@ -174,5 +180,7 @@ export const useSettings = () => {
|
|||
setButtonLanguage,
|
||||
disabledCards,
|
||||
setDisabledCards,
|
||||
threeDSecure,
|
||||
setThreeDSecure,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ const defaultPersistent = Object.freeze( {
|
|||
subtotalAdjustment: 'no_details', // [correction|no_details] Handling for subtotal mismatches
|
||||
landingPage: 'any', // [any|login|guest_checkout] PayPal checkout landing page
|
||||
buttonLanguage: '', // Language for PayPal buttons
|
||||
threeDSecure: 'only-required-3d-secure', // [no-3d-secure|only-required-3d-secure|always-3d-secure] 3D Secure settings
|
||||
|
||||
// Boolean flags.
|
||||
authorizeOnly: false, // Whether to only authorize payments initially
|
||||
|
|
|
@ -120,8 +120,12 @@ return array(
|
|||
return new PaymentSettings();
|
||||
},
|
||||
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
|
||||
$environment = $container->get( 'settings.environment' );
|
||||
assert( $environment instanceof Environment );
|
||||
|
||||
return new SettingsModel(
|
||||
$container->get( 'settings.service.sanitizer' )
|
||||
$container->get( 'settings.service.sanitizer' ),
|
||||
$environment->is_sandbox() ? $container->get( 'wcgateway.settings.invoice-prefix-random' ) : $container->get( 'wcgateway.settings.invoice-prefix' )
|
||||
);
|
||||
},
|
||||
'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
|
||||
|
|
|
@ -240,40 +240,7 @@ class PaymentMethodsDefinition {
|
|||
'title' => __( 'Advanced Credit and Debit Card Payments', 'woocommerce-paypal-payments' ),
|
||||
'description' => __( "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", 'woocommerce-paypal-payments' ),
|
||||
'icon' => 'payment-method-advanced-cards',
|
||||
'fields' => array(
|
||||
'threeDSecure' => array(
|
||||
'type' => 'radio',
|
||||
'default' => $this->settings->get_three_d_secure(),
|
||||
'label' => __( '3D Secure', 'woocommerce-paypal-payments' ),
|
||||
'description' => __(
|
||||
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __(
|
||||
'No 3D Secure',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'value' => 'no-3d-secure',
|
||||
),
|
||||
array(
|
||||
'label' => __(
|
||||
'Only when required',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'value' => 'only-required-3d-secure',
|
||||
),
|
||||
array(
|
||||
'label' => __(
|
||||
'Always require 3D Secure',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'value' => 'always-3d-secure',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'fields' => array(),
|
||||
);
|
||||
$group[] = array(
|
||||
'id' => AxoGateway::ID,
|
||||
|
|
|
@ -38,7 +38,6 @@ class PaymentSettings extends AbstractDataModel {
|
|||
protected function get_defaults() : array {
|
||||
return array(
|
||||
'paypal_show_logo' => false,
|
||||
'three_d_secure' => 'no-3d-secure',
|
||||
'fastlane_cardholder_name' => false,
|
||||
'fastlane_display_watermark' => false,
|
||||
'venmo_enabled' => false,
|
||||
|
@ -158,15 +157,6 @@ class PaymentSettings extends AbstractDataModel {
|
|||
return (bool) $this->data['paypal_show_logo'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 3DSecure.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_three_d_secure() : string {
|
||||
return $this->data['three_d_secure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Fastlane cardholder name.
|
||||
*
|
||||
|
@ -213,16 +203,6 @@ class PaymentSettings extends AbstractDataModel {
|
|||
$this->data['paypal_show_logo'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 3DSecure.
|
||||
*
|
||||
* @param string $value The value.
|
||||
* @return void
|
||||
*/
|
||||
public function set_three_d_secure( string $value ) : void {
|
||||
$this->data['three_d_secure'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Fastlane cardholder name.
|
||||
*
|
||||
|
|
|
@ -40,6 +40,13 @@ class SettingsModel extends AbstractDataModel {
|
|||
*/
|
||||
public const LANDING_PAGE_OPTIONS = array( 'any', 'login', 'guest_checkout' );
|
||||
|
||||
/**
|
||||
* Valid options for 3D Secure.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public const THREE_D_SECURE_OPTIONS = array( 'no-3d-secure', 'only-required-3d-secure', 'always-3d-secure' );
|
||||
|
||||
/**
|
||||
* Data sanitizer service.
|
||||
*
|
||||
|
@ -47,14 +54,24 @@ class SettingsModel extends AbstractDataModel {
|
|||
*/
|
||||
protected DataSanitizer $sanitizer;
|
||||
|
||||
/**
|
||||
* Invoice prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $invoice_prefix;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataSanitizer $sanitizer Data sanitizer service.
|
||||
* @param string $invoice_prefix Invoice prefix.
|
||||
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
|
||||
*/
|
||||
public function __construct( DataSanitizer $sanitizer ) {
|
||||
$this->sanitizer = $sanitizer;
|
||||
public function __construct( DataSanitizer $sanitizer, string $invoice_prefix ) {
|
||||
$this->sanitizer = $sanitizer;
|
||||
$this->invoice_prefix = $invoice_prefix;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -66,7 +83,7 @@ class SettingsModel extends AbstractDataModel {
|
|||
protected function get_defaults() : array {
|
||||
return array(
|
||||
// Free-form string values.
|
||||
'invoice_prefix' => '',
|
||||
'invoice_prefix' => $this->invoice_prefix,
|
||||
'brand_name' => '',
|
||||
'soft_descriptor' => '',
|
||||
|
||||
|
@ -74,6 +91,7 @@ class SettingsModel extends AbstractDataModel {
|
|||
'subtotal_adjustment' => 'correction', // Options: [correction|no_details].
|
||||
'landing_page' => 'any', // Options: [any|login|guest_checkout].
|
||||
'button_language' => '', // empty or a language locale code.
|
||||
'three_d_secure' => 'no-3d-secure', // Options: [no-3d-secure|only-required-3d-secure|always-3d-secure].
|
||||
|
||||
// Boolean flags.
|
||||
'authorize_only' => false,
|
||||
|
@ -200,6 +218,24 @@ class SettingsModel extends AbstractDataModel {
|
|||
$this->data['button_language'] = $this->sanitizer->sanitize_text( $language );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 3D Secure setting.
|
||||
*
|
||||
* @return string The 3D Secure setting.
|
||||
*/
|
||||
public function get_three_d_secure() : string {
|
||||
return $this->data['three_d_secure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 3D Secure setting.
|
||||
*
|
||||
* @param string $setting The 3D Secure setting to set.
|
||||
*/
|
||||
public function set_three_d_secure( string $setting ) : void {
|
||||
$this->data['three_d_secure'] = $this->sanitizer->sanitize_enum( $setting, self::THREE_D_SECURE_OPTIONS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authorize only setting.
|
||||
*
|
||||
|
|
|
@ -77,10 +77,6 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
'js_name' => 'paypalShowLogo',
|
||||
'sanitize' => 'to_boolean',
|
||||
),
|
||||
'three_d_secure' => array(
|
||||
'js_name' => 'threeDSecure',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
'fastlane_cardholder_name' => array(
|
||||
'js_name' => 'fastlaneCardholderName',
|
||||
'sanitize' => 'to_boolean',
|
||||
|
@ -207,7 +203,6 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
}
|
||||
|
||||
$gateway_settings['paypalShowLogo'] = $this->payment_settings->get_paypal_show_logo();
|
||||
$gateway_settings['threeDSecure'] = $this->payment_settings->get_three_d_secure();
|
||||
$gateway_settings['fastlaneCardholderName'] = $this->payment_settings->get_fastlane_cardholder_name();
|
||||
$gateway_settings['fastlaneDisplayWatermark'] = $this->payment_settings->get_fastlane_display_watermark();
|
||||
|
||||
|
|
|
@ -93,6 +93,10 @@ class SettingsRestEndpoint extends RestEndpoint {
|
|||
'disabled_cards' => array(
|
||||
'js_name' => 'disabledCards',
|
||||
),
|
||||
'three_d_secure' => array(
|
||||
'js_name' => 'threeDSecure',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -176,7 +176,35 @@ class ScriptDataHandler {
|
|||
'label' => _x( 'Hiper', 'Name of credit card', 'woocommerce-paypal-payments' ),
|
||||
),
|
||||
);
|
||||
$transformed_button_choices = array_map(
|
||||
|
||||
$three_d_secure_options = array(
|
||||
array(
|
||||
'value' => 'no-3d-secure',
|
||||
'label' => __( 'No 3D Secure', 'woocommerce-paypal-payments' ),
|
||||
'description' => __(
|
||||
'Do not use 3D Secure authentication for any transactions.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
),
|
||||
array(
|
||||
'value' => 'only-required-3d-secure',
|
||||
'label' => __( 'Only when required', 'woocommerce-paypal-payments' ),
|
||||
'description' => __(
|
||||
'Use 3D Secure when required by the card issuer or payment processor.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
),
|
||||
array(
|
||||
'value' => 'always-3d-secure',
|
||||
'label' => __( 'Always require 3D Secure', 'woocommerce-paypal-payments' ),
|
||||
'description' => __(
|
||||
'Always authenticate transactions with 3D Secure when available.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$transformed_button_choices = array_map(
|
||||
function( $key, $value ) {
|
||||
return array(
|
||||
'value' => $key,
|
||||
|
@ -198,6 +226,7 @@ class ScriptDataHandler {
|
|||
'storeCountry' => $this->store_country,
|
||||
'buttonLanguageChoices' => $transformed_button_choices,
|
||||
'disabledCardsChoices' => $disabled_cards_choices,
|
||||
'threeDSecureOptions' => $three_d_secure_options,
|
||||
);
|
||||
|
||||
if ( $is_pay_later_configurator_available ) {
|
||||
|
@ -230,5 +259,3 @@ class ScriptDataHandler {
|
|||
wp_dequeue_script( 'ppcp-paypal-subscription' );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -269,7 +269,6 @@ class SettingsDataManager {
|
|||
// Enable BCDC for business sellers without ACDC.
|
||||
$this->payment_methods->toggle_method_state( CardButtonGateway::ID, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to modify apm payment gateway states before saving.
|
||||
*
|
||||
|
|
|
@ -31,6 +31,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
|
|||
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
|
||||
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
|
||||
|
@ -576,7 +577,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
// Enable APMs after onboarding if the country is compatible.
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_toggle_payment_gateways_apms',
|
||||
function ( PaymentSettings $payment_methods, array $methods_apm ) use ( $container ) {
|
||||
function ( PaymentSettings $payment_methods, array $methods_apm, ConfigurationFlagsDTO $flags ) use ( $container ) {
|
||||
|
||||
$general_settings = $container->get( 'settings.data.general' );
|
||||
assert( $general_settings instanceof GeneralSettings );
|
||||
|
@ -586,6 +587,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
|
||||
// Enable all APM methods.
|
||||
foreach ( $methods_apm as $method ) {
|
||||
if ( $flags->use_card_payments === false ) {
|
||||
$payment_methods->toggle_method_state( $method['id'], $flags->use_card_payments );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip PayUponInvoice if merchant is not in Germany.
|
||||
if ( PayUponInvoiceGateway::ID === $method['id'] && 'DE' !== $merchant_country ) {
|
||||
continue;
|
||||
|
@ -606,7 +612,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
}
|
||||
},
|
||||
10,
|
||||
2
|
||||
3
|
||||
);
|
||||
|
||||
// Toggle payment gateways after onboarding based on flags.
|
||||
|
@ -635,6 +641,25 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
}
|
||||
);
|
||||
|
||||
// Migration code to update BN code of merchants that are on whitelabel mode (own_brand_only false) to use the whitelabel BN code (direct).
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_gateway_migrate_on_update',
|
||||
static function() use ( $container ) {
|
||||
$general_settings = $container->get( 'settings.data.general' );
|
||||
assert( $general_settings instanceof GeneralSettings );
|
||||
|
||||
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
|
||||
assert( $partner_attribution instanceof PartnerAttribution );
|
||||
|
||||
$own_brand_only = $general_settings->own_brand_only();
|
||||
$installation_path = $general_settings->get_installation_path();
|
||||
|
||||
if ( ! $own_brand_only && $installation_path !== InstallationPathEnum::DIRECT ) {
|
||||
$partner_attribution->initialize_bn_code( InstallationPathEnum::DIRECT, true );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -2021,6 +2021,49 @@ return array(
|
|||
return new TaskRegistrar();
|
||||
},
|
||||
|
||||
'wcgateway.settings.wc-tasks.pay-later-task-config' => static function( ContainerInterface $container ): array {
|
||||
$section_id = PayPalGateway::ID;
|
||||
$pay_later_tab_id = Settings::PAY_LATER_TAB_ID;
|
||||
|
||||
if ( $container->has( 'paylater-configurator.is-available' ) && $container->get( 'paylater-configurator.is-available' ) ) {
|
||||
return array(
|
||||
array(
|
||||
'id' => 'pay-later-messaging-task',
|
||||
'title' => __( 'Configure PayPal Pay Later messaging', 'woocommerce-paypal-payments' ),
|
||||
'description' => __( 'Decide where you want dynamic Pay Later messaging to show up and how you want it to look on your site.', 'woocommerce-paypal-payments' ),
|
||||
'redirect_url' => admin_url( "admin.php?page=wc-settings&tab=checkout§ion={$section_id}&ppcp-tab={$pay_later_tab_id}" ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array();
|
||||
},
|
||||
|
||||
'wcgateway.settings.wc-tasks.connect-task-config' => static function( ContainerInterface $container ): array {
|
||||
$is_connected = $container->get( 'settings.flag.is-connected' );
|
||||
$is_current_country_send_only = $container->get( 'wcgateway.is-send-only-country' );
|
||||
|
||||
if ( ! $is_connected && ! $is_current_country_send_only ) {
|
||||
return array(
|
||||
array(
|
||||
'id' => 'connect-to-paypal-task',
|
||||
'title' => __( 'Connect PayPal to complete setup', 'woocommerce-paypal-payments' ),
|
||||
'description' => __( 'PayPal Payments is almost ready. To get started, connect your account with the Activate PayPal Payments button.', 'woocommerce-paypal-payments' ),
|
||||
'redirect_url' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array();
|
||||
},
|
||||
|
||||
'wcgateway.settings.wc-tasks.task-config-services' => static function(): array {
|
||||
return array(
|
||||
'wcgateway.settings.wc-tasks.pay-later-task-config',
|
||||
'wcgateway.settings.wc-tasks.connect-task-config',
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* A configuration for simple redirect wc tasks.
|
||||
*
|
||||
|
@ -2032,18 +2075,14 @@ return array(
|
|||
* }>
|
||||
*/
|
||||
'wcgateway.settings.wc-tasks.simple-redirect-tasks-config' => static function( ContainerInterface $container ): array {
|
||||
$section_id = PayPalGateway::ID;
|
||||
$pay_later_tab_id = Settings::PAY_LATER_TAB_ID;
|
||||
|
||||
$list_of_config = array();
|
||||
$task_config_services = $container->get( 'wcgateway.settings.wc-tasks.task-config-services' );
|
||||
|
||||
if ( $container->has( 'paylater-configurator.is-available' ) && $container->get( 'paylater-configurator.is-available' ) ) {
|
||||
$list_of_config[] = array(
|
||||
'id' => 'pay-later-messaging-task',
|
||||
'title' => __( 'Configure PayPal Pay Later messaging', 'woocommerce-paypal-payments' ),
|
||||
'description' => __( 'Decide where you want dynamic Pay Later messaging to show up and how you want it to look on your site.', 'woocommerce-paypal-payments' ),
|
||||
'redirect_url' => admin_url( "admin.php?page=wc-settings&tab=checkout§ion={$section_id}&ppcp-tab={$pay_later_tab_id}" ),
|
||||
);
|
||||
foreach ( $task_config_services as $service_id ) {
|
||||
if ( $container->has( $service_id ) ) {
|
||||
$task_config = $container->get( $service_id );
|
||||
$list_of_config = array_merge( $list_of_config, $task_config );
|
||||
}
|
||||
}
|
||||
|
||||
return $list_of_config;
|
||||
|
@ -2092,4 +2131,29 @@ return array(
|
|||
'wcgateway.settings.admin-settings-enabled' => static function( ContainerInterface $container ): bool {
|
||||
return $container->has( 'settings.url' ) && ! SettingsModule::should_use_the_old_ui();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a prefix for the site, ensuring the same site always gets the same prefix (unless the URL changes).
|
||||
*/
|
||||
'wcgateway.settings.invoice-prefix' => static function( ContainerInterface $container ): string {
|
||||
$site_url = get_site_url( get_current_blog_id() );
|
||||
$hash = md5( $site_url );
|
||||
$letters = preg_replace( '~\d~', '', $hash ) ?? '';
|
||||
$prefix = substr( $letters, 0, 6 );
|
||||
|
||||
return $prefix ? $prefix . '-' : '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns random 6 characters length alphabetic prefix, followed by a hyphen.
|
||||
*/
|
||||
'wcgateway.settings.invoice-prefix-random' => static function( ContainerInterface $container ): string {
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$prefix = '';
|
||||
for ( $i = 0; $i < 6; $i++ ) {
|
||||
$prefix .= $characters[ wp_rand( 0, strlen( $characters ) - 1 ) ];
|
||||
}
|
||||
|
||||
return $prefix . '-';
|
||||
},
|
||||
);
|
||||
|
|
|
@ -69,7 +69,7 @@ class ConnectAdminNotice {
|
|||
$message = sprintf(
|
||||
/* translators: %1$s the gateway name. */
|
||||
__(
|
||||
'PayPal Payments is almost ready. To get started, connect your account with the <b>Activate PayPal</b> button <a href="%1$s">on the Account Setup page</a>.',
|
||||
'PayPal Payments is almost ready. To get started, connect your account with the <b>Activate PayPal Payments</b> button <a href="%1$s">on the Account Setup page</a>.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID )
|
||||
|
@ -77,6 +77,16 @@ class ConnectAdminNotice {
|
|||
return new Message( $message, 'warning' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current page is plugins.php.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_current_page_plugins_page(): bool {
|
||||
global $pagenow;
|
||||
return isset( $pagenow ) && $pagenow === 'plugins.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the message should display.
|
||||
*
|
||||
|
@ -87,6 +97,8 @@ class ConnectAdminNotice {
|
|||
* @return bool
|
||||
*/
|
||||
protected function should_display(): bool {
|
||||
return ! $this->is_connected && ! $this->is_current_country_send_only;
|
||||
return $this->is_current_page_plugins_page()
|
||||
&& ! $this->is_connected
|
||||
&& ! $this->is_current_country_send_only;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,9 @@ return function ( ContainerInterface $container, array $fields ): array {
|
|||
$onboarding_send_only_notice_renderer = $container->get( 'onboarding.render-send-only-notice' );
|
||||
assert( $onboarding_send_only_notice_renderer instanceof OnboardingSendOnlyNoticeRenderer );
|
||||
|
||||
$environment = $container->get( 'settings.environment' );
|
||||
assert( $environment instanceof Environment );
|
||||
|
||||
$is_send_only_country = $container->get( 'wcgateway.is-send-only-country' );
|
||||
$onboarding_elements_class = $is_send_only_country ? 'hide' : 'ppcp-onboarding-element';
|
||||
$send_only_country_notice_class = $is_send_only_country ? 'ppcp-onboarding-element' : 'hide';
|
||||
|
@ -510,13 +513,7 @@ return function ( ContainerInterface $container, array $fields ): array {
|
|||
'custom_attributes' => array(
|
||||
'pattern' => '[a-zA-Z_\\-]+',
|
||||
),
|
||||
'default' => ( static function (): string {
|
||||
$site_url = get_site_url( get_current_blog_id() );
|
||||
$hash = md5( $site_url );
|
||||
$letters = preg_replace( '~\d~', '', $hash ) ?? '';
|
||||
$prefix = substr( $letters, 0, 6 );
|
||||
return $prefix ? $prefix . '-' : '';
|
||||
} )(),
|
||||
'default' => $environment->is_sandbox() ? $container->get( 'wcgateway.settings.invoice-prefix-random' ) : $container->get( 'wcgateway.settings.invoice-prefix' ),
|
||||
'screens' => array(
|
||||
State::STATE_START,
|
||||
State::STATE_ONBOARDED,
|
||||
|
|
335
tests/integration/PHPUnit/IntegrationMockedTestCase.php
Normal file
335
tests/integration/PHPUnit/IntegrationMockedTestCase.php
Normal file
|
@ -0,0 +1,335 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WC_Order;
|
||||
use WC_Order_Item_Product;
|
||||
use WC_Payment_Token_CC;
|
||||
use WC_Subscription;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
|
||||
use WooCommerce\PayPalCommerce\Helper\RedirectorStub;
|
||||
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||
use WooCommerce\PayPalCommerce\PPCP;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
||||
class IntegrationMockedTestCase extends TestCase
|
||||
{
|
||||
use MockeryPHPUnitIntegration;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->default_product_id = $this->createAProductIfNotProvided();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $customer_id
|
||||
* @param string $payment_method
|
||||
* @param int $product_id
|
||||
* @param bool $set_paid
|
||||
* @return \WC_Order|\WP_Error
|
||||
* @throws \WC_Data_Exception
|
||||
*/
|
||||
public function getMockedOrder(int $customer_id, string $payment_method, int $product_id, bool $set_paid = true)
|
||||
{
|
||||
$order = wc_create_order([
|
||||
'customer_id' => $customer_id,
|
||||
'set_paid' => $set_paid,
|
||||
'billing' => [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'address_1' => '969 Market',
|
||||
'address_2' => '',
|
||||
'city' => 'San Francisco',
|
||||
'state' => 'CA',
|
||||
'postcode' => '94103',
|
||||
'country' => 'US',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '(555) 555-5555'
|
||||
],
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => $product_id,
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
]);
|
||||
$order->set_payment_method($payment_method);
|
||||
// Make sure the order is properly saved
|
||||
$order->save();
|
||||
|
||||
// Add the product to the order
|
||||
$item = new WC_Order_Item_Product();
|
||||
$item->set_props([
|
||||
'product_id' => $product_id,
|
||||
'quantity' => 1,
|
||||
'subtotal' => 10,
|
||||
'total' => 10,
|
||||
]);
|
||||
$order->add_item($item);
|
||||
$order->calculate_totals();
|
||||
$order->save();
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sku
|
||||
* @return int
|
||||
*/
|
||||
public function createAProductIfNotProvided(string $sku = 'DUMMY SUB SKU'): int
|
||||
{
|
||||
$product_id = wc_get_product_id_by_sku($sku);
|
||||
if (!$product_id) {
|
||||
$product = new \WC_Product_Subscription();
|
||||
$product->set_props([
|
||||
'name' => 'Dummy Subscription Product',
|
||||
'regular_price' => 10,
|
||||
'price' => 10,
|
||||
'sku' => 'DUMMY SUB SKU',
|
||||
'manage_stock' => false,
|
||||
'tax_status' => 'taxable',
|
||||
'downloadable' => false,
|
||||
'virtual' => false,
|
||||
'stock_status' => 'instock',
|
||||
'weight' => '1.1',
|
||||
// Subscription-specific properties
|
||||
'subscription_period' => 'month',
|
||||
'subscription_period_interval' => 1,
|
||||
'subscription_length' => 0, // 0 means unlimited
|
||||
'subscription_trial_period' => '',
|
||||
'subscription_trial_length' => 0,
|
||||
'subscription_price' => 10,
|
||||
'subscription_sign_up_fee' => 0,
|
||||
]);
|
||||
$product->save();
|
||||
$product_id = $product->get_id();
|
||||
}
|
||||
return $product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, callable> $overriddenServices
|
||||
* @return ContainerInterface
|
||||
*/
|
||||
protected function bootstrapModule(array $overriddenServices = []): ContainerInterface
|
||||
{
|
||||
$overriddenServices = array_merge([
|
||||
'http.redirector' => function () {
|
||||
return new RedirectorStub();
|
||||
}
|
||||
], $overriddenServices);
|
||||
|
||||
|
||||
$module = new class ($overriddenServices) implements ServiceModule, ExecutableModule {
|
||||
use ModuleClassNameIdTrait;
|
||||
|
||||
public function __construct(array $services)
|
||||
{
|
||||
$this->services = $services;
|
||||
}
|
||||
|
||||
public function services(): array
|
||||
{
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
public function run(ContainerInterface $c): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$rootDir = ROOT_DIR;
|
||||
$bootstrap = require("$rootDir/bootstrap.php");
|
||||
$appContainer = $bootstrap($rootDir, [], [$module]);
|
||||
|
||||
PPCP::init($appContainer);
|
||||
|
||||
return $appContainer;
|
||||
}
|
||||
|
||||
public function createCustomerIfNotExists(int $customer_id= 1): int
|
||||
{
|
||||
$customer = new \WC_Customer($customer_id);
|
||||
if ( empty($customer->get_email() )) {
|
||||
$customer->set_email('customer'. $customer_id. '@example.com');
|
||||
$customer->set_first_name('John');
|
||||
$customer->set_last_name('Doe');
|
||||
$customer->save();
|
||||
}
|
||||
return $customer->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment token for a customer.
|
||||
*
|
||||
* @param int $customer_id The customer ID.
|
||||
* @return WC_Payment_Token_CC The created payment token.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createAPaymentTokenForTheCustomer(int $customer_id = 1, $gateway_id = 'ppcp-gateway'): WC_Payment_Token_CC
|
||||
{
|
||||
$this->createCustomerIfNotExists($customer_id);
|
||||
|
||||
$token = new WC_Payment_Token_CC();
|
||||
$token->set_token('test_token_' . uniqid()); // Unique token ID
|
||||
$token->set_gateway_id($gateway_id);
|
||||
$token->set_user_id($customer_id);
|
||||
|
||||
// These fields are required for WC_Payment_Token_CC
|
||||
$token->set_card_type('visa'); // lowercase is often expected
|
||||
$token->set_last4('1234');
|
||||
$token->set_expiry_month('12');
|
||||
$token->set_expiry_year('2030'); // Missing expiry year in your original code
|
||||
|
||||
$result = $token->save();
|
||||
|
||||
if (!$result || is_wp_error($result)) {
|
||||
throw new \Exception('Failed to save payment token: ' .
|
||||
(is_wp_error($result) ? $result->get_error_message() : 'Unknown error'));
|
||||
}
|
||||
|
||||
$saved_token = \WC_Payment_Tokens::get($token->get_id());
|
||||
if (!$saved_token || $saved_token->get_id() !== $token->get_id()) {
|
||||
throw new \Exception('Token was not saved correctly');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a subscription for testing.
|
||||
*
|
||||
* @param int $customer_id The customer ID
|
||||
* @param string $payment_method The payment method
|
||||
* @param string $sku
|
||||
* @return WC_Subscription
|
||||
* @throws \WC_Data_Exception
|
||||
*/
|
||||
public function createSubscription(int $customer_id = 1, string $payment_method = 'ppcp-gateway', $sku = 'DUMMY SUB SKU'): WC_Subscription
|
||||
{
|
||||
// Create a product if not provided
|
||||
$product_id = $this->createAProductIfNotProvided($sku);
|
||||
|
||||
$order = $this->getMockedOrder($customer_id, $payment_method, $product_id, $set_paid = true);
|
||||
|
||||
$subscription = new WC_Subscription();
|
||||
$subscription->set_customer_id($customer_id);
|
||||
$subscription->set_payment_method($payment_method);
|
||||
$subscription->set_status('active');
|
||||
$subscription->set_parent_id($order->get_id());
|
||||
$subscription->set_billing_period('month');
|
||||
$subscription->set_billing_interval(1);
|
||||
|
||||
// Add a product to the subscription
|
||||
$subscription_item = new WC_Order_Item_Product();
|
||||
$subscription_item->set_props([
|
||||
'product_id' => $product_id,
|
||||
'quantity' => 1,
|
||||
'subtotal' => 10,
|
||||
'total' => 10,
|
||||
]);
|
||||
$subscription->add_item($subscription_item);
|
||||
$subscription->set_date_created(current_time('mysql'));
|
||||
$subscription->set_start_date(current_time('mysql'));
|
||||
$subscription->set_next_payment_date(date('Y-m-d H:i:s', strtotime('+1 month', current_time('timestamp'))));
|
||||
$subscription->save();
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a renewal order for testing
|
||||
*
|
||||
* @param int $customer_id
|
||||
* @param string $gateway_id
|
||||
* @param int $subscription_id
|
||||
* @return WC_Order
|
||||
*/
|
||||
protected function createRenewalOrder(int $customer_id, string $gateway_id, int $subscription_id): WC_Order
|
||||
{
|
||||
$renewal_order = $this->getMockedOrder($customer_id, $gateway_id, $this->default_product_id, false);
|
||||
$renewal_order->update_meta_data('_subscription_renewal', $subscription_id);
|
||||
$renewal_order->save();
|
||||
|
||||
return $renewal_order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the OrderEndpoint to return a successful/failed order.
|
||||
*
|
||||
* @param string $intent The order intent (CAPTURE or AUTHORIZE)
|
||||
* @param bool $success Whether the order was successful
|
||||
* @return object The mocked OrderEndpoint
|
||||
*/
|
||||
public function mockOrderEndpoint(string $intent = 'CAPTURE', bool $success = true): object
|
||||
{
|
||||
$order_endpoint = \Mockery::mock(OrderEndpoint::class)->shouldIgnoreMissing();
|
||||
$order = \Mockery::mock(Order::class)->shouldIgnoreMissing();
|
||||
|
||||
$order->shouldReceive('id')->andReturn('TEST-ORDER-' . uniqid());
|
||||
$order->shouldReceive('intent')->andReturn($intent);
|
||||
|
||||
$order_status = \Mockery::mock(OrderStatus::class)->shouldIgnoreMissing();
|
||||
$order_status->shouldReceive('is')->andReturn($success);
|
||||
$order_status->shouldReceive('name')->andReturn($success ? 'COMPLETED' : 'FAILED');
|
||||
$order->shouldReceive('status')->andReturn($order_status);
|
||||
|
||||
$payment_source = \Mockery::mock(PaymentSource::class)->shouldIgnoreMissing();
|
||||
$payment_source->shouldReceive('name')->andReturn('card');
|
||||
$order->shouldReceive('payment_source')->andReturn($payment_source);
|
||||
|
||||
$purchase_unit = \Mockery::mock(PurchaseUnit::class)->shouldIgnoreMissing();
|
||||
$payments = \Mockery::mock(Payments::class)->shouldIgnoreMissing();
|
||||
$capture = \Mockery::mock(Capture::class)->shouldIgnoreMissing();
|
||||
|
||||
$capture->shouldReceive('id')->andReturn('TEST-CAPTURE-' . uniqid());
|
||||
$capture_status = \Mockery::mock(CaptureStatus::class)->shouldIgnoreMissing();
|
||||
|
||||
$capture_status->shouldReceive('name')->andReturn($success ? 'COMPLETED' : 'DECLINED');
|
||||
$capture->shouldReceive('status')->andReturn($capture_status);
|
||||
|
||||
// Mock authorizations for AUTHORIZE intent
|
||||
if ($intent === 'AUTHORIZE') {
|
||||
$authorization = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization::class)->shouldIgnoreMissing();
|
||||
|
||||
$authorization->shouldReceive('id')->andReturn('TEST-AUTH-' . uniqid());
|
||||
$auth_status = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus::class)->shouldIgnoreMissing();
|
||||
|
||||
$auth_status->shouldReceive('name')->andReturn($success ? 'CREATED' : 'DENIED');
|
||||
$auth_status->shouldReceive('is')->andReturn($success);
|
||||
$authorization->shouldReceive('status')->andReturn($auth_status);
|
||||
$payments->shouldReceive('authorizations')->andReturn([$authorization]);
|
||||
$payments->shouldReceive('captures')->andReturn([]);
|
||||
} else {
|
||||
// For CAPTURE intent, set up captures but no authorizations
|
||||
$payments->shouldReceive('captures')->andReturn([$capture]);
|
||||
$payments->shouldReceive('authorizations')->andReturn([]);
|
||||
}
|
||||
|
||||
$purchase_unit->shouldReceive('payments')->andReturn($payments);
|
||||
$order->shouldReceive('purchase_units')->andReturn([$purchase_unit]);
|
||||
|
||||
// Set up the order endpoint methods
|
||||
$order_endpoint->shouldReceive('create')->andReturn($order);
|
||||
if ($intent === 'AUTHORIZE') {
|
||||
$order_endpoint->shouldReceive('authorize')->andReturn($order);
|
||||
} else {
|
||||
$order_endpoint->shouldReceive('capture')->andReturn($order);
|
||||
}
|
||||
$order_endpoint->shouldReceive('order')->andReturn($order);
|
||||
|
||||
return $order_endpoint;
|
||||
}
|
||||
}
|
|
@ -2,79 +2,390 @@
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WC_Product_Simple;
|
||||
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
|
||||
|
||||
/**
|
||||
* @group subscriptions
|
||||
* @group subscription-paypal
|
||||
* @group skip-ci
|
||||
*/
|
||||
class PayPalSubscriptionsRenewalTest extends TestCase {
|
||||
public function test_renewal_order_is_not_created_just_after_receiving_webhook() {
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
|
||||
class PayPalSubscriptionsRenewalTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Tests that renewal orders are not created for recent subscriptions.
|
||||
*
|
||||
* GIVEN a subscription created 1 minute ago
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN no renewal order should be created
|
||||
*/
|
||||
public function test_renewal_order_is_not_created_just_after_receiving_webhook()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
// Simulates receiving webhook 1 minute after subscription start.
|
||||
$subscription = $this->createSubscription( '-1 minute' );
|
||||
$subscription = $this->createSubscription('-1 minute');
|
||||
|
||||
$handler->process( [ $subscription ], 'TRANSACTION-ID' );
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals( count( $renewal ), 0 );
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(0, count($renewal), 'No renewal order should be created for a subscription that is only 1 minute old');
|
||||
}
|
||||
|
||||
public function test_renewal_order_is_created_when_receiving_webhook_nine_hours_later() {
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
|
||||
/**
|
||||
* Tests that renewal orders are created for subscriptions older than 8 hours.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN a renewal order should be created
|
||||
*/
|
||||
public function test_renewal_order_is_created_when_receiving_webhook_nine_hours_later()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
// Simulates receiving webhook 9 hours after subscription start.
|
||||
$subscription = $this->createSubscription( '-9 hour' );
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
|
||||
$handler->process( [ $subscription ], 'TRANSACTION-ID' );
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals( count( $renewal ), 1 );
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal), 'A renewal order should be created for a subscription that is 9 hours old');
|
||||
}
|
||||
|
||||
private function createSubscription( string $startDate ) {
|
||||
$order = wc_create_order( [
|
||||
'customer_id' => 1,
|
||||
'set_paid' => true,
|
||||
/**
|
||||
* Tests that renewal orders are created when subscription has renewal meta.
|
||||
*
|
||||
* GIVEN a subscription created 5 minutes ago
|
||||
* AND the subscription has the _ppcp_is_subscription_renewal meta set to 'true'
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN a renewal order should be created
|
||||
*/
|
||||
public function test_renewal_order_is_created_when_subscription_has_renewal_meta()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
// Create a subscription that's only 5 minutes old (would normally not trigger renewal)
|
||||
$subscription = $this->createSubscription('-5 minute');
|
||||
|
||||
// But mark it as needing renewal
|
||||
$subscription->update_meta_data('_ppcp_is_subscription_renewal', 'true');
|
||||
$subscription->save_meta_data();
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal), 'A renewal order should be created when subscription has _ppcp_is_subscription_renewal meta set to true, regardless of age');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that renewal order payment method matches the subscription.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago
|
||||
* AND the subscription has a specific payment method
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN a renewal order should be created
|
||||
* AND the renewal order should have the same payment method as the subscription
|
||||
*/
|
||||
public function test_renewal_order_payment_method_matches_subscription()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
$payment_method = 'ppcp-gateway';
|
||||
$subscription->set_payment_method($payment_method);
|
||||
$subscription->save();
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal_ids = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal_ids), 'A renewal order should be created for a subscription that is 9 hours old');
|
||||
|
||||
$renewal_order = wc_get_order(reset($renewal_ids));
|
||||
$this->assertEquals($payment_method, $renewal_order->get_payment_method(), 'The renewal order should have the same payment method as the subscription');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that renewal orders are marked as paid.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN a renewal order should be created
|
||||
* AND the renewal order should be marked as paid
|
||||
*/
|
||||
public function test_renewal_order_is_marked_as_paid()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal_ids = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal_ids), 'A renewal order should be created for a subscription that is 9 hours old');
|
||||
|
||||
$renewal_order = wc_get_order(reset($renewal_ids));
|
||||
$this->assertTrue($renewal_order->is_paid(), 'The renewal order should be marked as paid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that transaction ID is set on renewal orders.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago
|
||||
* AND a unique transaction ID
|
||||
* WHEN the process method is called with this subscription and transaction ID
|
||||
* THEN a renewal order should be created
|
||||
* AND the renewal order should have the transaction ID set
|
||||
*/
|
||||
public function test_transaction_id_is_set_on_renewal_order()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
$transaction_id = 'TEST-TRANSACTION-ID-' . uniqid();
|
||||
|
||||
$handler->process([$subscription], $transaction_id);
|
||||
$renewal_ids = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal_ids), 'A renewal order should be created for a subscription that is 9 hours old');
|
||||
|
||||
$renewal_order = wc_get_order(reset($renewal_ids));
|
||||
$this->assertEquals($transaction_id, $renewal_order->get_transaction_id(), 'The renewal order should have the transaction ID set correctly');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that subscription status is set to on-hold before renewal.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago with 'active' status
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN the subscription status should be changed to 'on-hold'
|
||||
*/
|
||||
public function test_subscription_status_is_set_to_on_hold_before_renewal()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
$initial_status = $subscription->get_status();
|
||||
$this->assertEquals('active', $initial_status, 'The subscription should start with active status');
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
|
||||
// Status should be on-hold before the renewal order is created
|
||||
$this->assertEquals('on-hold', $subscription->get_status(), 'The subscription status should be changed to on-hold before renewal');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that transaction ID is set on parent order when no renewal is created.
|
||||
*
|
||||
* GIVEN a subscription created 1 minute ago
|
||||
* AND a unique transaction ID
|
||||
* WHEN the process method is called with this subscription and transaction ID
|
||||
* THEN no renewal order should be created
|
||||
* AND the transaction ID should be set on the parent order
|
||||
*/
|
||||
public function test_transaction_id_is_set_on_parent_order_when_no_renewal()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-1 minute');
|
||||
|
||||
$transaction_id = 'PARENT-TRANSACTION-ID-' . uniqid();
|
||||
$parent_order_id = $subscription->get_parent_id();
|
||||
|
||||
$handler->process([$subscription], $transaction_id);
|
||||
|
||||
// No renewal order should be created
|
||||
$renewal = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(0, count($renewal), 'No renewal order should be created for a subscription that is only 1 minute old');
|
||||
|
||||
//use latest order to get the updated status
|
||||
$parent_order = wc_get_order($parent_order_id);
|
||||
// Transaction ID should be set on parent order
|
||||
$this->assertEquals($transaction_id, $parent_order->get_transaction_id(), 'The transaction ID should be set on the parent order when no renewal is created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that subscription meta is set when processing parent order.
|
||||
*
|
||||
* GIVEN a subscription created 1 minute ago
|
||||
* AND the subscription has no _ppcp_is_subscription_renewal meta
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN the _ppcp_is_subscription_renewal meta should be set to 'true'
|
||||
*/
|
||||
public function test_subscription_meta_is_set_when_processing_parent_order()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-1 minute');
|
||||
|
||||
// Meta should not exist before processing
|
||||
$this->assertEmpty($subscription->get_meta('_ppcp_is_subscription_renewal'), 'The subscription should not have _ppcp_is_subscription_renewal meta before processing');
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
|
||||
// Meta should be set after processing
|
||||
$this->assertEquals('true', $subscription->get_meta('_ppcp_is_subscription_renewal'), 'The _ppcp_is_subscription_renewal meta should be set to true after processing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests handling subscriptions without valid parent orders.
|
||||
*
|
||||
* GIVEN a subscription created 9 hours ago
|
||||
* AND the parent order is not available
|
||||
* WHEN the process method is called with this subscription
|
||||
* THEN a renewal order should still be created
|
||||
* AND the renewal order should be properly set up with transaction ID
|
||||
* AND the subscription status should be set to 'on-hold'
|
||||
*/
|
||||
public function test_subscription_without_valid_parent_order()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
$transaction_id = 'TEST-TRANSACTION-ID-' . uniqid();
|
||||
|
||||
// Simulate a scenario where the parent order doesn't exist or is not a WC_Order
|
||||
// Mock wc_get_order to return false instead of a WC_Order instance
|
||||
add_filter('woocommerce_get_shop_order_args', function ($args) use ($subscription) {
|
||||
if (isset($args['id']) && $args['id'] === $subscription->get_parent_id()) {
|
||||
return ['return' => false]; // This causes wc_get_order to return false
|
||||
}
|
||||
return $args;
|
||||
});
|
||||
|
||||
// Process should not throw any errors
|
||||
$handler->process([$subscription], $transaction_id);
|
||||
|
||||
// Verify that a renewal order was created (as the subscription is 9 hours old)
|
||||
$renewal_ids = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(1, count($renewal_ids), 'A renewal order should be created even when the parent order is not available');
|
||||
|
||||
// Verify the renewal order was properly set up
|
||||
$renewal_order = wc_get_order(reset($renewal_ids));
|
||||
$this->assertTrue($renewal_order->is_paid(), 'The renewal order should be marked as paid even when the parent order is not available');
|
||||
$this->assertEquals($transaction_id, $renewal_order->get_transaction_id(), 'The renewal order should have the transaction ID set correctly even when the parent order is not available');
|
||||
|
||||
// Verify no errors occurred due to invalid parent order
|
||||
$this->assertEquals('on-hold', $subscription->get_status(), 'The subscription status should be set to on-hold even when the parent order is not available');
|
||||
|
||||
// Remove the filter
|
||||
remove_all_filters('woocommerce_get_shop_order_args');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that parent order transaction ID is updated for non-renewal subscriptions.
|
||||
*
|
||||
* GIVEN a subscription created 1 minute ago
|
||||
* AND the parent order has no transaction ID
|
||||
* WHEN the process method is called with this subscription and a unique transaction ID
|
||||
* THEN the parent order's transaction ID should be updated
|
||||
* AND the subscription should be marked for future renewal
|
||||
* AND no renewal order should be created
|
||||
*/
|
||||
public function test_parent_order_transaction_id_is_updated_when_processing_non_renewal_subscription()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$logger = $c->get('woocommerce.logger.woocommerce');
|
||||
$handler = new RenewalHandler($logger);
|
||||
|
||||
// Create a subscription that's not ready for renewal
|
||||
$subscription = $this->createSubscription('-1 minute');
|
||||
|
||||
// Get the parent order
|
||||
$parent_order_id = $subscription->get_parent_id();
|
||||
$parent_order = wc_get_order($parent_order_id);
|
||||
|
||||
$this->assertEmpty($parent_order->get_transaction_id(), 'The parent order should not have a transaction ID before processing');
|
||||
|
||||
$transaction_id = 'PARENT-ORDER-TRANSACTION-' . uniqid();
|
||||
$handler->process([$subscription], $transaction_id);
|
||||
$parent_order = wc_get_order($parent_order_id);
|
||||
|
||||
$this->assertEquals($transaction_id, $parent_order->get_transaction_id(), 'The parent order transaction ID should be updated correctly');
|
||||
|
||||
$this->assertEquals('true', $subscription->get_meta('_ppcp_is_subscription_renewal'), 'The subscription should be marked for future renewal after processing');
|
||||
|
||||
$renewal_orders = $subscription->get_related_orders('ids', array('renewal'));
|
||||
$this->assertEquals(0, count($renewal_orders), 'No renewal order should be created for an empty array of subscriptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the RenewalHandler correctly handles an empty array of subscriptions.
|
||||
*
|
||||
* GIVEN the RenewalHandler with a mocked logger
|
||||
* WHEN the process method is called with an empty array of subscriptions
|
||||
* THEN no exceptions should be thrown
|
||||
* AND the logger should not be called
|
||||
*/
|
||||
public function test_process_empty_subscriptions_array()
|
||||
{
|
||||
// Create a logger mock that expects no operations if no subscriptions
|
||||
$logger_mock = \Mockery::mock(LoggerInterface::class);
|
||||
// The logger should not be called at all with an empty array
|
||||
$logger_mock->shouldNotReceive('info');
|
||||
|
||||
$handler = new RenewalHandler($logger_mock);
|
||||
$transaction_id = 'TEST-TRANSACTION-EMPTY-ARRAY';
|
||||
|
||||
// Process an empty array of subscriptions
|
||||
$handler->process([], $transaction_id);
|
||||
|
||||
// Test is successful if no exceptions are thrown
|
||||
// and the mock expectations are met (logger not called)
|
||||
$this->assertTrue(true, 'No exceptions were thrown when processing an empty array of subscriptions');
|
||||
}
|
||||
|
||||
private function createSubscription(string $startDate)
|
||||
{
|
||||
$order = wc_create_order([
|
||||
'customer_id' => 1,
|
||||
'set_paid' => true,
|
||||
'payment_method' => 'ppcp-gateway',
|
||||
'billing' => [
|
||||
'billing' => [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'address_1' => '969 Market',
|
||||
'address_2' => '',
|
||||
'city' => 'San Francisco',
|
||||
'state' => 'CA',
|
||||
'postcode' => '94103',
|
||||
'country' => 'US',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '(555) 555-5555'
|
||||
'last_name' => 'Doe',
|
||||
'address_1' => '969 Market',
|
||||
'address_2' => '',
|
||||
'city' => 'San Francisco',
|
||||
'state' => 'CA',
|
||||
'postcode' => '94103',
|
||||
'country' => 'US',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '(555) 555-5555'
|
||||
],
|
||||
'line_items' => [
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => 42,
|
||||
'quantity' => 1
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
] );
|
||||
]);
|
||||
// Make sure the order is properly saved
|
||||
$order->save();
|
||||
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_props([
|
||||
'name' => 'Dummy Product',
|
||||
'name' => 'Dummy Product',
|
||||
'regular_price' => 10,
|
||||
'price' => 10,
|
||||
'sku' => 'DUMMY SKU',
|
||||
'manage_stock' => false,
|
||||
'tax_status' => 'taxable',
|
||||
'downloadable' => false,
|
||||
'virtual' => false,
|
||||
'stock_status' => 'instock',
|
||||
'weight' => '1.1',
|
||||
'price' => 10,
|
||||
'sku' => 'DUMMY SKU',
|
||||
'manage_stock' => false,
|
||||
'tax_status' => 'taxable',
|
||||
'downloadable' => false,
|
||||
'virtual' => false,
|
||||
'stock_status' => 'instock',
|
||||
'weight' => '1.1',
|
||||
]);
|
||||
|
||||
return wcs_create_subscription([
|
||||
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
|
||||
'parent_id' => $order->get_id(),
|
||||
$subscription = wcs_create_subscription([
|
||||
'start_date' => gmdate('Y-m-d H:i:s', strtotime($startDate)),
|
||||
'order_id' => $order->get_id(),
|
||||
'customer_id' => 1,
|
||||
'status' => 'active',
|
||||
'billing_period' => 'day',
|
||||
|
@ -87,5 +398,9 @@ class PayPalSubscriptionsRenewalTest extends TestCase {
|
|||
]
|
||||
],
|
||||
]);
|
||||
|
||||
// Make sure the subscription is properly saved
|
||||
$subscription->save();
|
||||
return $subscription;
|
||||
}
|
||||
}
|
||||
|
|
263
tests/integration/PHPUnit/VaultingSubscriptionsTest.php
Normal file
263
tests/integration/PHPUnit/VaultingSubscriptionsTest.php
Normal file
|
@ -0,0 +1,263 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WC_Payment_Token;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\State;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
||||
/**
|
||||
* @group subscriptions
|
||||
* @group subscription-vaulting
|
||||
* @group skip-ci
|
||||
*/
|
||||
class VaultingSubscriptionsTest extends IntegrationMockedTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Common mock setup
|
||||
$this->mockPaymentTokensEndpoint = \Mockery::mock(PaymentTokensEndpoint::class);
|
||||
|
||||
// Create customer and default product that can be reused
|
||||
$this->customer_id = $this->createCustomerIfNotExists();
|
||||
$this->default_product_id = $this->createAProductIfNotProvided();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a test container with common mocks
|
||||
*
|
||||
* @param OrderEndpoint $orderEndpoint
|
||||
* @param array $additionalServices Additional services to override
|
||||
* @return ContainerInterface
|
||||
*/
|
||||
protected function setupTestContainer(OrderEndpoint $orderEndpoint, array $additionalServices = []): ContainerInterface
|
||||
{
|
||||
$services = [
|
||||
'api.endpoint.order' => function () use ($orderEndpoint) {
|
||||
return $orderEndpoint;
|
||||
},
|
||||
'api.endpoint.payment-tokens' => function () {
|
||||
return $this->mockPaymentTokensEndpoint;
|
||||
}
|
||||
];
|
||||
|
||||
return $this->bootstrapModule(array_merge($services, $additionalServices));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment token and configures the mock endpoint to return it
|
||||
*
|
||||
* @param int $customer_id
|
||||
* @param string $gateway_id
|
||||
* @return WC_Payment_Token
|
||||
*/
|
||||
protected function setupPaymentToken(int $customer_id, string $gateway_id = PayPalGateway::ID): WC_Payment_Token
|
||||
{
|
||||
$paymentToken = $this->createAPaymentTokenForTheCustomer($customer_id, $gateway_id);
|
||||
|
||||
$this->mockPaymentTokensEndpoint->shouldReceive('payment_tokens_for_customer')
|
||||
->andReturn([
|
||||
[
|
||||
'id' => $paymentToken->get_token(),
|
||||
'payment_source' => new PaymentSource(
|
||||
'card',
|
||||
(object)[
|
||||
'last_digits' => $paymentToken->get_last4(),
|
||||
'brand' => $paymentToken->get_card_type(),
|
||||
'expiry' => $paymentToken->get_expiry_year() . '-' . $paymentToken->get_expiry_month()
|
||||
]
|
||||
)
|
||||
]
|
||||
]);
|
||||
|
||||
return $paymentToken;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that vaulting is automatically enabled when subscription mode is set to vaulting_api.
|
||||
*
|
||||
* GIVEN a PayPal account with Reference Transactions enabled
|
||||
* WHEN the subscription mode is set to "vaulting_api"
|
||||
* THEN vaulting should be automatically enabled for the PayPal gateway
|
||||
*/
|
||||
public function test_vaulting_is_enabled_when_subscription_mode_is_vaulting_api()
|
||||
{
|
||||
$user_has_cap_callback = function ($allcaps, $caps, $args) {
|
||||
if (isset($args[0]) && $args[0] === 'manage_woocommerce') {
|
||||
$allcaps['manage_woocommerce'] = true;
|
||||
}
|
||||
return $allcaps;
|
||||
};
|
||||
add_filter('user_has_cap', $user_has_cap_callback, 10, 3);
|
||||
|
||||
// Convert to Mockery mocks
|
||||
$billing_agreements_endpoint_mock = \Mockery::mock(BillingAgreementsEndpoint::class);
|
||||
$billing_agreements_endpoint_mock->shouldReceive('reference_transaction_enabled')
|
||||
->andReturn(true);
|
||||
|
||||
$state_mock = \Mockery::mock(State::class);
|
||||
$state_mock->shouldReceive('current_state')
|
||||
->andReturn(State::STATE_ONBOARDED);
|
||||
|
||||
$token_mock = \Mockery::mock(Token::class);
|
||||
$token_mock->shouldReceive('vaulting_available')
|
||||
->andReturn(true);
|
||||
|
||||
$bearer_mock = \Mockery::mock(Bearer::class);
|
||||
$bearer_mock->shouldReceive('bearer')
|
||||
->andReturn($token_mock);
|
||||
|
||||
// Create and configure the SettingsListener
|
||||
$c = $this->bootstrapModule([
|
||||
'api.endpoint.billing-agreements' => function () use ($billing_agreements_endpoint_mock) {
|
||||
return $billing_agreements_endpoint_mock;
|
||||
},
|
||||
'onboarding.state' => function () use ($state_mock) {
|
||||
return $state_mock;
|
||||
},
|
||||
'wcgateway.current-ppcp-settings-page-id' => function () {
|
||||
return '123';
|
||||
},
|
||||
'api.bearer' => function () use ($bearer_mock) {
|
||||
return $bearer_mock;
|
||||
},
|
||||
]);
|
||||
|
||||
$settings = $c->get('wcgateway.settings');
|
||||
|
||||
// Store original settings to restore later
|
||||
$original_subscription_mode = $settings->get('subscriptions_mode');
|
||||
$original_vault_enabled = $settings->get('vault_enabled');
|
||||
|
||||
try {
|
||||
$settings_listener = $c->get('wcgateway.settings.listener');
|
||||
$settings_listener->listen_for_vaulting_enabled();
|
||||
$_POST['ppcp'] = [
|
||||
'subscriptions_mode' => 'vaulting_api',
|
||||
'vault_enabled' => '0' // Explicitly set to disabled
|
||||
];
|
||||
$_REQUEST['_wpnonce'] = wp_create_nonce('ppcp-settings');
|
||||
$settings_listener->listen_for_vaulting_enabled();
|
||||
|
||||
// THEN vaulting should be automatically enabled for the PayPal gateway
|
||||
$this->assertTrue(
|
||||
get_option('woocommerce-ppcp-settings')['vault_enabled'],
|
||||
'Vaulting should be automatically enabled when subscription mode is set to vaulting_api'
|
||||
);
|
||||
|
||||
} finally {
|
||||
unset($_POST['ppcp']);
|
||||
$settings->set('subscriptions_mode', $original_subscription_mode);
|
||||
$settings->set('vault_enabled', $original_vault_enabled);
|
||||
$settings->persist();
|
||||
remove_filter('user_has_cap', $user_has_cap_callback, 10);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Data provider for payment gateway tests
|
||||
*/
|
||||
public function paymentGatewayProvider(): array
|
||||
{
|
||||
return [
|
||||
'PayPal Gateway' => [PayPalGateway::ID],
|
||||
'Credit Card Gateway' => [CreditCardGateway::ID]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests PayPal renewal payment processing.
|
||||
*
|
||||
* GIVEN a subscription with a saved PayPal payment token due for renewal
|
||||
* WHEN the renewal process is triggered
|
||||
* THEN a new PayPal order should be created using the customer token
|
||||
*
|
||||
* @dataProvider paymentGatewayProvider
|
||||
*/
|
||||
public function test_renewal_payment_processing(string $gateway_id)
|
||||
{
|
||||
$mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', true);
|
||||
$c = $this->setupTestContainer($mockOrderEndpoint);
|
||||
$this->setupPaymentToken($this->customer_id, $gateway_id);
|
||||
$subscription = $this->createSubscription($this->customer_id, $gateway_id);
|
||||
$renewal_order = $this->createRenewalOrder($this->customer_id, $gateway_id, $subscription->get_id());
|
||||
|
||||
$renewal_handler = $c->get('wc-subscriptions.renewal-handler');
|
||||
$renewal_handler->renew($renewal_order);
|
||||
|
||||
// Check that the order was processed
|
||||
$this->assertEquals('processing', $renewal_order->get_status(), 'The renewal order should be processing after successful payment');
|
||||
$this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID');
|
||||
}
|
||||
/**
|
||||
* Tests that renewal processing handles failed payments correctly.
|
||||
*
|
||||
* GIVEN a subscription due for renewal
|
||||
* WHEN the payment process fails with an exception
|
||||
* THEN the renewal order should be marked as failed
|
||||
*/
|
||||
public function test_renewal_handles_failed_payment()
|
||||
{
|
||||
$mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false);
|
||||
$c = $this->setupTestContainer($mockOrderEndpoint);
|
||||
$this->setupPaymentToken($this->customer_id);
|
||||
$subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID);
|
||||
$renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id());
|
||||
$renewal_handler = $c->get('wc-subscriptions.renewal-handler');
|
||||
$renewal_handler->renew($renewal_order);
|
||||
|
||||
// Check that the order status is failed
|
||||
$this->assertEquals('failed', $renewal_order->get_status(), 'The renewal order should be marked as failed when payment fails');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests authorization-only subscription renewals.
|
||||
*
|
||||
* GIVEN the payment intent is set to "AUTHORIZE"
|
||||
* WHEN a subscription renewal payment is processed
|
||||
* THEN the payment should be authorized but not captured
|
||||
*/
|
||||
public function test_authorize_only_subscription_renewal()
|
||||
{
|
||||
// Mock the OrderEndpoint with AUTHORIZE intent
|
||||
$mockOrderEndpoint = $this->mockOrderEndpoint('AUTHORIZE', true);
|
||||
$c = $this->setupTestContainer($mockOrderEndpoint);
|
||||
|
||||
// Setup payment token and subscription
|
||||
$this->setupPaymentToken($this->customer_id);
|
||||
$subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID);
|
||||
$renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id());
|
||||
|
||||
// Override the intent setting to ensure it's set to AUTHORIZE
|
||||
$settings = $c->get('wcgateway.settings');
|
||||
$original_intent = $settings->get('intent');
|
||||
$settings->set('intent', 'authorize');
|
||||
$settings->persist();
|
||||
|
||||
try {
|
||||
// Process the renewal
|
||||
$renewal_handler = $c->get('wc-subscriptions.renewal-handler');
|
||||
$renewal_handler->renew($renewal_order);
|
||||
|
||||
// Check that the order was processed with authorization
|
||||
$this->assertEquals('on-hold', $renewal_order->get_status(), 'The renewal order should be on-hold after successful authorization');
|
||||
$this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID');
|
||||
$this->assertEquals('AUTHORIZE', $mockOrderEndpoint->order('')->intent(), 'The order intent should be AUTHORIZE');
|
||||
} finally {
|
||||
// Restore original settings
|
||||
$settings->set('intent', $original_intent);
|
||||
$settings->persist();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,3 +21,6 @@ define('WP_ROOT_DIR', $wpRootDir);
|
|||
$_SERVER['HTTP_HOST'] = ''; // just to avoid a warning
|
||||
|
||||
require_once WP_ROOT_DIR . '/wp-load.php';
|
||||
// Ensure the TestCase class is loaded
|
||||
require_once __DIR__ . '/TestCase.php';
|
||||
require_once __DIR__ . '/IntegrationMockedTestCase.php';
|
||||
|
|
|
@ -96,6 +96,8 @@ function clear_plugin_branding( ContainerInterface $container ) : void {
|
|||
*/
|
||||
delete_option( 'woocommerce_paypal_branded' );
|
||||
|
||||
delete_option( 'ppcp_bn_code' );
|
||||
|
||||
try {
|
||||
$general_settings = $container->get( 'settings.data.general' );
|
||||
assert( $general_settings instanceof GeneralSettings );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue