Merge pull request #1920 from woocommerce/vault-v3-wc-subscriptions

Vault v3 WC Subscriptions integration (2481)
This commit is contained in:
Emili Castells 2024-01-05 14:57:30 +01:00 committed by GitHub
commit 843bee60f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 263 deletions

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
@ -79,27 +78,6 @@ class SavePaymentMethodsModule implements ModuleInterface {
add_filter(
'ppcp_create_order_request_body_data',
function( array $data, string $payment_method, array $request_data ): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
if ( $wc_order_action === 'wcs_process_renewal' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
$subscription = wcs_get_subscription( (int) $subscription_id );
if ( $subscription ) {
$customer_id = $subscription->get_customer_id();
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
foreach ( $wc_tokens as $token ) {
$data['payment_source'] = array(
'paypal' => array(
'vault_id' => $token->get_token(),
),
);
return $data;
}
}
}
if ( $payment_method === CreditCardGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
@ -114,6 +92,10 @@ class SavePaymentMethodsModule implements ModuleInterface {
);
$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,
@ -189,7 +171,6 @@ class SavePaymentMethodsModule implements ModuleInterface {
);
add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' );
add_filter( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false' );
add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' );
add_action(
@ -215,6 +196,9 @@ class SavePaymentMethodsModule implements ModuleInterface {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$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 );
}
}
$id_token = $api->id_token( $target_customer_id );
@ -354,6 +338,9 @@ class SavePaymentMethodsModule implements ModuleInterface {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$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 );
}
}
$id_token = $api->id_token( $target_customer_id );

View file

@ -9,7 +9,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WC_Order;
use WC_Subscription;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
@ -24,6 +26,7 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
@ -194,32 +197,60 @@ class RenewalHandler {
'renewal'
);
$token = $this->get_token_for_customer( $customer, $wc_order );
if ( $token ) {
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$stored_credentials = array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
// Vault v3.
$payment_source = null;
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), PayPalGateway::ID );
foreach ( $wc_tokens as $token ) {
$payment_source = new PaymentSource(
'paypal',
(object) array(
'vault_id' => $token->get_token(),
)
);
$subscriptions = wcs_get_subscriptions_for_renewal_order( $wc_order );
foreach ( $subscriptions as $post_id => $subscription ) {
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$stored_credentials['previous_transaction_reference'] = $previous_transaction_reference;
break;
}
}
$payment_source = new PaymentSource(
'card',
(object) array(
'vault_id' => $token->id(),
'stored_credential' => $stored_credentials,
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID );
foreach ( $wc_tokens as $token ) {
$payment_source = $this->card_payment_source( $token->get_token(), $wc_order );
}
}
if ( $payment_source ) {
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
$payer,
null,
'',
ApplicationContext::USER_ACTION_CONTINUE,
'',
array(),
$payment_source
);
$this->handle_paypal_order( $wc_order, $order );
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
return;
}
// Vault v2.
$token = $this->get_token_for_customer( $customer, $wc_order );
if ( $token ) {
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$payment_source = $this->card_payment_source( $token->id(), $wc_order );
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
@ -244,6 +275,7 @@ class RenewalHandler {
return;
}
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
@ -259,28 +291,8 @@ class RenewalHandler {
$wc_order->get_id()
)
);
return;
}
if ( apply_filters( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', true ) ) {
return;
}
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
$payer
);
$this->handle_paypal_order( $wc_order, $order );
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
}
/**
@ -302,18 +314,7 @@ class RenewalHandler {
$tokens = $this->repository->all_for_user_id( (int) $customer->get_id() );
if ( ! $tokens ) {
$error_message = sprintf(
'Payment failed. No payment tokens found for customer %d.',
$customer->get_id()
);
$wc_order->update_status(
'failed',
$error_message
);
$this->logger->error( $error_message );
return false;
}
$subscription = function_exists( 'wcs_get_subscription' ) ? wcs_get_subscription( $wc_order->get_meta( '_subscription_renewal' ) ) : null;
@ -399,4 +400,44 @@ class RenewalHandler {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}
}
/**
* Returns a Card payment source.
*
* @param string $token Vault token id.
* @param WC_Order $wc_order WC order.
* @return PaymentSource
* @throws NotFoundException If setting is not found.
*/
private function card_payment_source( string $token, WC_Order $wc_order ): PaymentSource {
$properties = array(
'vault_id' => $token,
);
if (
$this->settings->has( '3d_secure_contingency' )
&& ( $this->settings->get( '3d_secure_contingency' ) === 'SCA_ALWAYS' || $this->settings->get( '3d_secure_contingency' ) === 'SCA_WHEN_REQUIRED' )
) {
$stored_credentials = array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
);
$subscriptions = wcs_get_subscriptions_for_renewal_order( $wc_order );
foreach ( $subscriptions as $post_id => $subscription ) {
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$stored_credentials['previous_transaction_reference'] = $previous_transaction_reference;
$properties['stored_credentials'] = $stored_credentials;
break;
}
}
}
return new PaymentSource(
'card',
(object) $properties
);
}
}

View file

@ -90,7 +90,9 @@ class WcSubscriptionsModule implements ModuleInterface {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
if ( ! $c->has( 'save-payment-methods.eligible' ) || ! $c->get( 'save-payment-methods.eligible' ) ) {
$this->add_payment_token_id( $subscription, $payment_token_repository, $logger );
}
if ( count( $subscription->get_related_orders() ) === 1 ) {
$parent_order = $subscription->get_parent();

View file

@ -1,171 +0,0 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Dictionary;
use Exception;
use Psr\Log\LoggerInterface;
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\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use function Brain\Monkey\Functions\when;
class RenewalHandlerTest extends TestCase
{
use MockeryPHPUnitIntegration;
private $logger;
private $repository;
private $orderEndpoint;
private $purchaseUnitFactory;
private $shippingPreferenceFactory;
private $payerFactory;
private $environment;
private $sut;
public function setUp(): void
{
parent::setUp();
$this->logger = Mockery::mock(LoggerInterface::class);
$this->repository = Mockery::mock(PaymentTokenRepository::class);
$this->orderEndpoint = Mockery::mock(OrderEndpoint::class);
$this->purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class);
$this->shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class);
$this->payerFactory = Mockery::mock(PayerFactory::class);
$this->environment = new Environment(new Dictionary([]));
$authorizedPaymentProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class);
$settings = Mockery::mock(Settings::class);
$settings
->shouldReceive('has')
->andReturnFalse();
$this->logger->shouldReceive('error')->andReturnUsing(function ($msg) {
throw new Exception($msg);
});
$this->logger->shouldReceive('info');
$this->sut = new RenewalHandler(
$this->logger,
$this->repository,
$this->orderEndpoint,
$this->purchaseUnitFactory,
$this->shippingPreferenceFactory,
$this->payerFactory,
$this->environment,
$settings,
$authorizedPaymentProcessor
);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testRenewProcessOrder()
{
$transactionId = 'ABC123';
$wcOrder = Mockery::mock(\WC_Order::class);
$customer = Mockery::mock('overload:WC_Customer');
$token = Mockery::mock(PaymentToken::class);
$payer = Mockery::mock(Payer::class);
$order = Mockery::mock(Order::class);
$capture = Mockery::mock(Capture::class);
$capture->expects('id')
->andReturn($transactionId);
$capture->expects('status')
->andReturn(new CaptureStatus(CaptureStatus::COMPLETED));
$payments = Mockery::mock(Payments::class);
$payments->shouldReceive('captures')
->andReturn([$capture]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit->shouldReceive('payments')
->andReturn($payments);
$order
->shouldReceive('id')
->andReturn('101');
$order->shouldReceive('intent')
->andReturn('CAPTURE');
$order->shouldReceive('status->is')
->andReturn(true);
$order
->shouldReceive('purchase_units')
->andReturn([$purchaseUnit]);
$order
->shouldReceive('payment_source')
->andReturn(null);
$wcOrder
->shouldReceive('get_payment_method')
->andReturn('');
$wcOrder
->shouldReceive('get_meta')
->andReturn('');
$wcOrder
->shouldReceive('get_id')
->andReturn(1);
$wcOrder
->shouldReceive('get_customer_id')
->andReturn(2);
$wcOrder
->expects('update_meta_data')
->with(PayPalGateway::ORDER_ID_META_KEY, '101');
$wcOrder
->expects('update_meta_data')
->with(PayPalGateway::INTENT_META_KEY, 'CAPTURE');
$wcOrder
->expects('update_meta_data')
->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live');
$wcOrder
->expects('payment_complete');
$wcOrder
->expects('set_transaction_id');
$this->repository->shouldReceive('all_for_user_id')
->andReturn([$token]);
$customer->shouldReceive('get_id')
->andReturn(1);
$this->purchaseUnitFactory->shouldReceive('from_wc_order')
->andReturn($purchaseUnit);
$this->payerFactory->shouldReceive('from_customer')
->andReturn($payer);
$this->shippingPreferenceFactory->shouldReceive('from_state')
->with($purchaseUnit, 'renewal')
->andReturn('no_shipping');
$this->orderEndpoint->shouldReceive('create')
->with([$purchaseUnit], 'no_shipping', $payer, $token)
->andReturn($order);
when('wcs_get_subscriptions_for_order')->justReturn(array());
$wcOrder->shouldReceive('update_status');
$wcOrder->shouldReceive('save');
$this->sut->renew($wcOrder);
}
}