Merge branch 'trunk' into PCP-2085-next-payment-status-not-updated-when-using-pay-pal-subscriptions

This commit is contained in:
Emili Castells Guasch 2024-04-12 10:05:58 +02:00
commit aeecb72589
68 changed files with 1695 additions and 330 deletions

View file

@ -22,6 +22,8 @@ use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -101,7 +103,10 @@ return array(
$api_shop_country,
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.place-order-button-text' )
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'vaulting.wc-payment-tokens' )
);
},
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -131,8 +136,10 @@ return array(
$vaulted_credit_card_handler,
$container->get( 'onboarding.environment' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'save-payment-methods.endpoint.capture-card-payment' ),
$container->get( 'wcgateway.endpoint.capture-card-payment' ),
$container->get( 'api.prefix' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.wc-payment-tokens' ),
$logger
);
},
@ -384,19 +391,25 @@ return array(
$notice = $container->get( 'wcgateway.notice.authorize-order-action' );
$settings = $container->get( 'wcgateway.settings' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$amount_factory = $container->get( 'api.factory.amount' );
return new AuthorizedPaymentsProcessor(
$order_endpoint,
$payments_endpoint,
$logger,
$notice,
$settings,
$subscription_helper
$subscription_helper,
$amount_factory
);
},
'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new RenderAuthorizeAction( $column );
},
'wcgateway.admin.render-reauthorize-action' => static function ( ContainerInterface $container ): RenderReauthorizeAction {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new RenderReauthorizeAction( $column );
},
'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new PaymentStatusOrderDetail( $column );
@ -952,10 +965,10 @@ return array(
'ideal' => _x( 'iDEAL', 'Name of payment method', 'woocommerce-paypal-payments' ),
'mybank' => _x( 'MyBank', 'Name of payment method', 'woocommerce-paypal-payments' ),
'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ),
'sofort' => _x( 'Sofort', 'Name of payment method', 'woocommerce-paypal-payments' ),
'venmo' => _x( 'Venmo', 'Name of payment method', 'woocommerce-paypal-payments' ),
'trustly' => _x( 'Trustly', 'Name of payment method', 'woocommerce-paypal-payments' ),
'paylater' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ),
'paylater' => _x( 'PayPal Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ),
'paypal' => _x( 'PayPal', 'Name of payment method', 'woocommerce-paypal-payments' ),
);
},
@ -978,6 +991,7 @@ return array(
array_flip(
array(
'paylater',
'paypal',
)
)
);
@ -1579,4 +1593,17 @@ return array(
)
);
},
'wcgateway.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment {
return new CaptureCardPayment(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helpers.real-time-account-updater' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -9,9 +9,6 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
/**
* Class RenderAuthorizeAction
*/

View file

@ -0,0 +1,67 @@
<?php
/**
* Renders the order action "Reauthorize PayPal payment"
*
* @package WooCommerce\PayPalCommerce\WcGateway\Admin
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
/**
* Class RenderReauthorizeAction
*/
class RenderReauthorizeAction {
/**
* The capture info column.
*
* @var OrderTablePaymentStatusColumn
*/
private $column;
/**
* PaymentStatusOrderDetail constructor.
*
* @param OrderTablePaymentStatusColumn $column The capture info column.
*/
public function __construct( OrderTablePaymentStatusColumn $column ) {
$this->column = $column;
}
/**
* Renders the action into the $order_actions array based on the WooCommerce order.
*
* @param array $order_actions The actions to render into.
* @param \WC_Order $wc_order The order for which to render the action.
*
* @return array
*/
public function render( array $order_actions, \WC_Order $wc_order ) : array {
if ( ! $this->should_render_for_order( $wc_order ) ) {
return $order_actions;
}
$order_actions['ppcp_reauthorize_order'] = esc_html__(
'Reauthorize PayPal payment',
'woocommerce-paypal-payments'
);
return $order_actions;
}
/**
* Whether the action should be rendered for a certain WooCommerce order.
*
* @param \WC_Order $order The Woocommerce order.
*
* @return bool
*/
private function should_render_for_order( \WC_Order $order ) : bool {
$status = $order->get_status();
$not_allowed_statuses = array( 'refunded', 'cancelled', 'failed' );
return $this->column->should_render_for_order( $order ) &&
! $this->column->is_captured( $order ) &&
! in_array( $status, $not_allowed_statuses, true );
}
}

View file

@ -0,0 +1,186 @@
<?php
/**
* The Capture Card Payment endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\RealTimeAccountUpdaterHelper;
use WP_Error;
/**
* Class CaptureCardPayment
*/
class CaptureCardPayment {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The order factory.
*
* @var OrderFactory
*/
private $order_factory;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* Real Time Account Updater helper.
*
* @var RealTimeAccountUpdaterHelper
*/
private $real_time_account_updater_helper;
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CaptureCardPayment constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param OrderFactory $order_factory The order factory.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param RealTimeAccountUpdaterHelper $real_time_account_updater_helper Real Time Account Updater helper.
* @param Settings $settings The settings.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
OrderFactory $order_factory,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler,
RealTimeAccountUpdaterHelper $real_time_account_updater_helper,
Settings $settings,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->order_factory = $order_factory;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->real_time_account_updater_helper = $real_time_account_updater_helper;
$this->settings = $settings;
$this->logger = $logger;
}
/**
* Creates PayPal order from the given card vault id.
*
* @param string $vault_id Vault id.
* @param string $custom_id Custom id.
* @param string $invoice_id Invoice id.
* @return stdClass
* @throws RuntimeException When request fails.
*/
public function create_order( string $vault_id, string $custom_id, string $invoice_id ): stdClass {
$intent = $this->settings->has( 'intent' ) && strtoupper( (string) $this->settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$items = array( $this->purchase_unit_factory->from_wc_cart() );
$data = array(
'intent' => $intent,
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( true, false );
},
$items
),
'payment_source' => array(
'card' => array(
'vault_id' => $vault_id,
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'UNSCHEDULED',
'usage' => 'SUBSEQUENT',
),
),
),
'custom_id' => $custom_id,
'invoice_id' => $invoice_id,
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
}
}

View file

@ -34,7 +34,7 @@ class FundingSourceRenderer {
*
* @var string[]
*/
protected $own_funding_sources = array( 'venmo', 'paylater' );
protected $own_funding_sources = array( 'venmo', 'paylater', 'paypal' );
/**
* FundingSourceRenderer constructor.
@ -63,7 +63,7 @@ class FundingSourceRenderer {
return $this->funding_sources[ $id ];
}
return sprintf(
/* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */
/* translators: %s - BLIK, iDeal, Mercado Pago, etc. */
__( '%s (via PayPal)', 'woocommerce-paypal-payments' ),
$this->funding_sources[ $id ]
);
@ -84,7 +84,7 @@ class FundingSourceRenderer {
if ( array_key_exists( $id, $this->funding_sources ) ) {
return sprintf(
/* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */
/* translators: %s - BLIK, iDeal, Mercado Pago, etc. */
__( 'Pay via %s.', 'woocommerce-paypal-payments' ),
$this->funding_sources[ $id ]
);

View file

@ -15,30 +15,33 @@ use WC_Order;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class CreditCardGateway
*/
class CreditCardGateway extends \WC_Payment_Gateway_CC {
use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait;
use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait;
const ID = 'ppcp-credit-card-gateway';
@ -154,6 +157,20 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
private $prefix;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* WooCommerce payment tokens factory.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/**
* The logger.
*
@ -179,6 +196,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param CaptureCardPayment $capture_card_payment Capture card payment.
* @param string $prefix The prefix.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -197,6 +216,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
OrderEndpoint $order_endpoint,
CaptureCardPayment $capture_card_payment,
string $prefix,
PaymentTokensEndpoint $payment_tokens_endpoint,
WooCommercePaymentTokens $wc_payment_tokens,
LoggerInterface $logger
) {
$this->id = self::ID;
@ -215,6 +236,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->order_endpoint = $order_endpoint;
$this->capture_card_payment = $capture_card_payment;
$this->prefix = $prefix;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->wc_payment_tokens = $wc_payment_tokens;
$this->logger = $logger;
if ( $state->current_state() === State::STATE_ONBOARDED ) {
@ -291,8 +314,10 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
public function form() {
add_action( 'gettext', array( $this, 'replace_credit_card_cvv_label' ), 10, 3 );
add_action( 'gettext', array( $this, 'replace_credit_card_cvv_placeholder' ), 10, 3 );
parent::form();
remove_action( 'gettext', 'replace_credit_card_cvv_label' );
remove_action( 'gettext', 'replace_credit_card_cvv_placeholder' );
}
/**
@ -312,6 +337,23 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
return __( 'CVV', 'woocommerce-paypal-payments' );
}
/**
* Replace WooCommerce credit card CVV field placeholder.
*
* @param string $translation Translated text.
* @param string $text Original text to translate.
* @param string $domain Text domain.
*
* @return string Translated field.
*/
public function replace_credit_card_cvv_placeholder( string $translation, string $text, string $domain ): string {
if ( 'woocommerce' !== $domain || 'CVC' !== $text || ! apply_filters( 'woocommerce_paypal_payments_card_fields_translate_card_cvv', true ) ) {
return $translation;
}
return __( 'CVV', 'woocommerce-paypal-payments' );
}
/**
* Returns the icons of the gateway.
*
@ -413,10 +455,39 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) );
if ( $this->is_free_trial_order( $wc_order ) && $card_payment_token_id ) {
$customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );
foreach ( $customer_tokens as $token ) {
if ( $token['payment_source']->name() === 'card' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
if ( $card_payment_token_id ) {
$customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), self::ID );
if ( $customer_tokens && empty( $wc_tokens ) ) {
$this->wc_payment_tokens->create_wc_tokens( $customer_tokens, get_current_user_id() );
}
$customer_token_ids = array();
foreach ( $customer_tokens as $customer_token ) {
$customer_token_ids[] = $customer_token['id'];
}
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $card_payment_token_id ) {
if ( ! in_array( $token->get_token(), $customer_token_ids, true ) ) {
$token->delete();
continue;
}
$custom_id = $wc_order->get_order_number();
$invoice_id = $this->prefix . $wc_order->get_order_number();
$create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id );

View file

@ -14,12 +14,14 @@ use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
@ -48,12 +50,18 @@ class PayPalGateway extends \WC_Payment_Gateway {
const ORDER_ID_META_KEY = '_ppcp_paypal_order_id';
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source';
const ORDER_PAYER_EMAIL_META_KEY = '_ppcp_paypal_payer_email';
const FEES_META_KEY = '_ppcp_paypal_fees';
const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees';
const REFUNDS_META_KEY = '_ppcp_refunds';
const THREE_D_AUTH_RESULT_META_KEY = '_ppcp_paypal_3DS_auth_result';
const FRAUD_RESULT_META_KEY = '_ppcp_paypal_fraud_result';
/**
* List of payment sources wich we are expected to store the payer email in the WC Order metadata.
*/
const PAYMENT_SOURCES_WITH_PAYER_EMAIL = array( 'paypal', 'paylater', 'venmo' );
/**
* The Settings Renderer.
*
@ -173,26 +181,50 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
private $paypal_checkout_url_factory;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* WooCommerce payment tokens.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/**
* PayPalGateway constructor.
*
* @param SettingsRenderer $settings_renderer The Settings Renderer.
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param OrderProcessor $order_processor The Order Processor.
* @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param LoggerInterface $logger The logger.
* @param string $api_shop_country The api shop country.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID.
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param SettingsRenderer $settings_renderer The Settings Renderer.
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param OrderProcessor $order_processor The Order Processor.
* @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param LoggerInterface $logger The logger.
* @param string $api_shop_country The api shop country.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID.
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -211,7 +243,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
string $api_shop_country,
OrderEndpoint $order_endpoint,
callable $paypal_checkout_url_factory,
string $place_order_button_text
string $place_order_button_text,
PaymentTokensEndpoint $payment_tokens_endpoint,
bool $vault_v3_enabled,
WooCommercePaymentTokens $wc_payment_tokens
) {
$this->id = self::ID;
$this->settings_renderer = $settings_renderer;
@ -231,6 +266,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->api_shop_country = $api_shop_country;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
$this->order_button_text = $place_order_button_text;
$this->order_endpoint = $order_endpoint;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->wc_payment_tokens = $wc_payment_tokens;
if ( $this->onboarded ) {
$this->supports = array( 'refunds', 'tokenization' );
@ -295,8 +334,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
'process_admin_options',
)
);
$this->order_endpoint = $order_endpoint;
}
/**
@ -496,7 +533,49 @@ class PayPalGateway extends \WC_Payment_Gateway {
$wc_order->save();
}
if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) && ! $this->subscription_helper->paypal_subscription_id() ) {
if (
'card' !== $funding_source
&& $this->is_free_trial_order( $wc_order )
&& ! $this->subscription_helper->paypal_subscription_id()
) {
$ppcp_guest_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null;
if ( $this->vault_v3_enabled && $ppcp_guest_payment_for_free_trial ) {
$customer_id = $ppcp_guest_payment_for_free_trial->customer->id ?? '';
if ( $customer_id ) {
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
}
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal ) ) {
$email = '';
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address ) ) {
$email = $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$ppcp_guest_payment_for_free_trial->id,
$email
);
}
WC()->session->set( 'ppcp_guest_payment_for_free_trial', null );
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
$customer_id = get_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$payment_source_name = $token['payment_source']->name() ?? '';
if ( $payment_source_name === 'paypal' || $payment_source_name === 'venmo' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
$user_id = (int) $wc_order->get_customer_id();
$tokens = $this->payment_token_repository->all_for_user_id( $user_id );
if ( ! array_filter(
@ -509,7 +588,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
}
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}

View file

@ -198,25 +198,41 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
'description' => __( "Specify brand name, logo and customer service instructions to be presented on Ratepay's payment instructions.", 'woocommerce-paypal-payments' ),
),
'brand_name' => array(
'title' => __( 'Brand name', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => get_bloginfo( 'name' ) ?? '',
'desc_tip' => true,
'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'title' => __( 'Brand name', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => get_bloginfo( 'name' ) ?? '',
'desc_tip' => true,
'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions. Should not exceed 127 characters.', 'woocommerce-paypal-payments' ),
'maxlength' => 127,
'custom_attributes' => array(
'pattern' => '.{1,127}',
'autocomplete' => 'off',
'required' => '',
),
),
'logo_url' => array(
'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ),
'type' => 'url',
'default' => '',
'desc_tip' => true,
'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ),
'type' => 'url',
'default' => '',
'desc_tip' => true,
'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'custom_attributes' => array(
'pattern' => '.+',
'autocomplete' => 'off',
'required' => '',
),
),
'customer_service_instructions' => array(
'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => '',
'desc_tip' => true,
'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => '',
'desc_tip' => true,
'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'custom_attributes' => array(
'pattern' => '.+',
'autocomplete' => 'off',
'required' => '',
),
),
);
}

View file

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use WC_Order;
@ -91,6 +93,20 @@ class AuthorizedPaymentsProcessor {
*/
private $subscription_helper;
/**
* The amount factory.
*
* @var AmountFactory
*/
private $amount_factory;
/**
* The reauthorization failure reason.
*
* @var string
*/
private $reauthorization_failure_reason = '';
/**
* AuthorizedPaymentsProcessor constructor.
*
@ -100,6 +116,7 @@ class AuthorizedPaymentsProcessor {
* @param AuthorizeOrderActionNotice $notice The notice.
* @param ContainerInterface $config The settings.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param AmountFactory $amount_factory The amount factory.
*/
public function __construct(
OrderEndpoint $order_endpoint,
@ -107,7 +124,8 @@ class AuthorizedPaymentsProcessor {
LoggerInterface $logger,
AuthorizeOrderActionNotice $notice,
ContainerInterface $config,
SubscriptionHelper $subscription_helper
SubscriptionHelper $subscription_helper,
AmountFactory $amount_factory
) {
$this->order_endpoint = $order_endpoint;
@ -116,6 +134,7 @@ class AuthorizedPaymentsProcessor {
$this->notice = $notice;
$this->config = $config;
$this->subscription_helper = $subscription_helper;
$this->amount_factory = $amount_factory;
}
/**
@ -249,6 +268,67 @@ class AuthorizedPaymentsProcessor {
}
}
/**
* Reauthorizes an authorized payment for an WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
*
* @return string The status or reauthorization id.
*/
public function reauthorize_payment( WC_Order $wc_order ): string {
$this->reauthorization_failure_reason = '';
try {
$order = $this->paypal_order_from_wc_order( $wc_order );
} catch ( Exception $exception ) {
$this->logger->error( 'Could not get PayPal order from WC order: ' . $exception->getMessage() );
if ( $exception->getCode() === 404 ) {
return self::NOT_FOUND;
}
return self::INACCESSIBLE;
}
$amount = $this->amount_factory->from_wc_order( $wc_order );
$authorizations = $this->all_authorizations( $order );
$uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations );
if ( ! $uncaptured_authorizations ) {
if ( $this->captured_authorizations( ...$authorizations ) ) {
$this->logger->info( 'Authorizations already captured.' );
return self::ALREADY_CAPTURED;
}
$this->logger->info( 'Bad authorization.' );
return self::BAD_AUTHORIZATION;
}
$authorization = end( $uncaptured_authorizations );
try {
$this->payments_endpoint->reauthorize( $authorization->id(), new Money( $amount->value(), $amount->currency_code() ) );
} catch ( PayPalApiException $exception ) {
$this->reauthorization_failure_reason = $exception->details()[0]->description ?? null;
$this->logger->error( 'Reauthorization failed: ' . $exception->name() . ' | ' . $this->reauthorization_failure_reason );
return self::FAILED;
} catch ( Exception $exception ) {
$this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() );
return self::FAILED;
}
return self::SUCCESSFUL;
}
/**
* The reason for a failed reauthorization.
*
* @return string
*/
public function reauthorization_failure_reason(): string {
return $this->reauthorization_failure_reason;
}
/**
* Voids authorizations for the given PayPal order.
*
@ -392,4 +472,5 @@ class AuthorizedPaymentsProcessor {
}
);
}
}

View file

@ -76,7 +76,7 @@ trait CreditCardOrderInfoHandlingTrait {
/**
* Fired when the 3DS information is added to WC order.
*/
do_action( 'woocommerce_paypal_payments_thee_d_secure_added', $wc_order, $order );
do_action( 'woocommerce_paypal_payments_three_d_secure_added', $wc_order, $order );
}
}
@ -96,8 +96,9 @@ trait CreditCardOrderInfoHandlingTrait {
return;
}
$fraud_responses = $fraud->to_array();
$card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
$fraud_responses = $fraud->to_array();
$card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
$card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' );
$avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' );
/* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */
@ -109,6 +110,7 @@ trait CreditCardOrderInfoHandlingTrait {
<li>%3$s</li>
</ul>
<li>%4$s</li>
<li>%5$s</li>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
@ -119,7 +121,9 @@ trait CreditCardOrderInfoHandlingTrait {
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) ),
/* translators: %s is card brand */
sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) )
sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ),
/* translators: %s card last digits */
sprintf( __( 'Card Last Digits: %s', 'woocommerce-paypal-payments' ), esc_html( $card_last_digits ) )
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
@ -136,7 +140,13 @@ trait CreditCardOrderInfoHandlingTrait {
);
$wc_order->add_order_note( $cvv_response_order_note );
$meta_details = array_merge( $fraud_responses, array( 'card_brand' => $card_brand ) );
$meta_details = array_merge(
$fraud_responses,
array(
'card_brand' => $card_brand,
'card_last_digits' => $card_last_digits,
)
);
$wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $meta_details );
$wc_order->save_meta_data();

View file

@ -45,6 +45,18 @@ trait OrderMetaTrait {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY, $payment_source );
}
$payer = $order->payer();
if (
$payer
&& $payment_source
&& in_array( $payment_source, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true )
) {
$payer_email = $payer->email_address();
if ( $payer_email ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email );
}
}
$wc_order->save();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );

View file

@ -65,7 +65,7 @@ return function ( ContainerInterface $container, array $fields ): array {
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="American Express" src="' . esc_url( $module_url ) . 'assets/images/amex.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="Discover" src="' . esc_url( $module_url ) . 'assets/images/discover.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="iDEAL" src="' . esc_url( $module_url ) . 'assets/images/ideal-dark.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="Sofort" src="' . esc_url( $module_url ) . 'assets/images/sofort.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="BLIK" src="' . esc_url( $module_url ) . 'assets/images/blik.svg"/></a>
</div>
<div class="ppcp-onboarding-header-apm-logos">
<a href="https://woo.com/document/woocommerce-paypal-payments/#apple-pay" target="_blank"><img alt="Apple Pay" src="' . esc_url( $module_url ) . 'assets/images/button-Apple-Pay.png"/></a>

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
@ -447,6 +448,53 @@ class WCGatewayModule implements ModuleInterface {
delete_transient( 'ppcp_reference_transaction_enabled' );
}
);
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_admin_billing_fields',
function ( $fields ) {
global $theorder;
if ( ! apply_filters( 'woocommerce_paypal_payments_order_details_show_paypal_email', true ) ) {
return $fields;
}
if ( ! is_array( $fields ) ) {
return $fields;
}
if ( ! $theorder instanceof WC_Order ) {
return $fields;
}
$email = $theorder->get_meta( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY ) ?: '';
if ( ! $email ) {
return $fields;
}
// Is payment source is paypal exclude all non paypal funding sources.
$payment_source = $theorder->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ?: '';
$is_paypal_funding_source = ( strpos( $theorder->get_payment_method_title(), '(via PayPal)' ) === false );
if ( $payment_source === 'paypal' && ! $is_paypal_funding_source ) {
return $fields;
}
$fields['paypal_email'] = array(
'label' => __( 'PayPal email address', 'woocommerce-paypal-payments' ),
'value' => $email,
'wrapper_class' => 'form-field-wide',
'custom_attributes' => array( 'disabled' => 'disabled' ),
);
return $fields;
}
);
}
/**
@ -614,13 +662,18 @@ class WCGatewayModule implements ModuleInterface {
return $order_actions;
}
$render = $container->get( 'wcgateway.admin.render-authorize-action' );
$render_reauthorize = $container->get( 'wcgateway.admin.render-reauthorize-action' );
$render_authorize = $container->get( 'wcgateway.admin.render-authorize-action' );
/**
* Renders the authorize action in the select field.
*
* @var RenderAuthorizeAction $render
*/
return $render->render( $order_actions, $theorder );
return $render_reauthorize->render(
$render_authorize->render( $order_actions, $theorder ),
$theorder
);
}
);
@ -637,6 +690,36 @@ class WCGatewayModule implements ModuleInterface {
$authorized_payments_processor->capture_authorized_payment( $wc_order );
}
);
add_action(
'woocommerce_order_action_ppcp_reauthorize_order',
static function ( WC_Order $wc_order ) use ( $container ) {
$admin_notices = $container->get( 'admin-notices.repository' );
assert( $admin_notices instanceof Repository );
/**
* The authorized payments processor.
*
* @var AuthorizedPaymentsProcessor $authorized_payments_processor
*/
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
if ( $authorized_payments_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) {
$message = sprintf(
'%1$s %2$s',
esc_html__( 'Reauthorization with PayPal failed: ', 'woocommerce-paypal-payments' ),
$authorized_payments_processor->reauthorization_failure_reason() ?: ''
);
$admin_notices->persist( new Message( $message, 'error' ) );
} else {
$admin_notices->persist( new Message( 'Payment reauthorized.', 'info' ) );
$wc_order->add_order_note(
__( 'Payment reauthorized.', 'woocommerce-paypal-payments' )
);
}
}
);
}
/**