Merge pull request #1980 from woocommerce/PCP-1685-store-three-d-secure-enrollment-status-and-authentication-status-responses-in-wc-order

Store three d secure enrollment status and authentication status responses in wc order (1685)
This commit is contained in:
Emili Castells 2024-02-08 16:16:06 +01:00 committed by GitHub
commit 53c2c96b8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 249 additions and 51 deletions

2
composer.lock generated
View file

@ -4996,5 +4996,5 @@
"ext-json": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -417,7 +417,7 @@ return array(
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
},
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
return new AuthorizationFactory( $container->get( 'api.factory.fraud-processor-response' ) );
},
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
return new ExchangeRateFactory();

View file

@ -28,19 +28,29 @@ class Authorization {
*/
private $authorization_status;
/**
* The fraud processor response (AVS, CVV ...).
*
* @var FraudProcessorResponse|null
*/
protected $fraud_processor_response;
/**
* Authorization constructor.
*
* @param string $id The id.
* @param AuthorizationStatus $authorization_status The status.
* @param string $id The id.
* @param AuthorizationStatus $authorization_status The status.
* @param FraudProcessorResponse|null $fraud_processor_response The fraud processor response (AVS, CVV ...).
*/
public function __construct(
string $id,
AuthorizationStatus $authorization_status
AuthorizationStatus $authorization_status,
?FraudProcessorResponse $fraud_processor_response
) {
$this->id = $id;
$this->authorization_status = $authorization_status;
$this->id = $id;
$this->authorization_status = $authorization_status;
$this->fraud_processor_response = $fraud_processor_response;
}
/**
@ -71,15 +81,30 @@ class Authorization {
$this->authorization_status->is( AuthorizationStatus::PENDING );
}
/**
* Returns the fraud processor response (AVS, CVV ...).
*
* @return FraudProcessorResponse|null
*/
public function fraud_processor_response() : ?FraudProcessorResponse {
return $this->fraud_processor_response;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
$data = array(
'id' => $this->id,
'status' => $this->authorization_status->name(),
);
if ( $this->fraud_processor_response ) {
$data['fraud_processor_response'] = $this->fraud_processor_response->to_array();
}
return $data;
}
}

View file

@ -19,6 +19,22 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
*/
class AuthorizationFactory {
/**
* The FraudProcessorResponseFactory factory.
*
* @var FraudProcessorResponseFactory
*/
protected $fraud_processor_response_factory;
/**
* AuthorizationFactory constructor.
*
* @param FraudProcessorResponseFactory $fraud_processor_response_factory The FraudProcessorResponseFactory factory.
*/
public function __construct( FraudProcessorResponseFactory $fraud_processor_response_factory ) {
$this->fraud_processor_response_factory = $fraud_processor_response_factory;
}
/**
* Returns an Authorization based off a PayPal response.
*
@ -42,12 +58,17 @@ class AuthorizationFactory {
$reason = $data->status_details->reason ?? null;
$fraud_processor_response = isset( $data->processor_response ) ?
$this->fraud_processor_response_factory->from_paypal_response( $data->processor_response )
: null;
return new Authorization(
$data->id,
new AuthorizationStatus(
$data->status,
$reason ? new AuthorizationStatusDetails( $reason ) : null
)
),
$fraud_processor_response
);
}
}

View file

@ -51,6 +51,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
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';
/**
* The Settings Renderer.

View file

@ -0,0 +1,147 @@
<?php
/**
* Common operations performed for handling the ACDC order info.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\FraudProcessorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Trait CreditCardOrderInfoHandlingTrait.
*/
trait CreditCardOrderInfoHandlingTrait {
/**
* Handles the 3DS details.
*
* Adds the order note with 3DS details.
* Adds the order meta with 3DS details.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WC order.
*/
protected function handle_three_d_secure(
Order $order,
WC_Order $wc_order
): void {
$payment_source = $order->payment_source();
if ( ! $payment_source || $payment_source->name() !== 'card' ) {
return;
}
$authentication_result = $payment_source->properties()->authentication_result ?? null;
$card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $authentication_result ) {
$card_authentication_result_factory = new CardAuthenticationResultFactory();
$result = $card_authentication_result_factory->from_paypal_response( $authentication_result );
$three_d_response_order_note_title = __( '3DS authentication result', 'woocommerce-paypal-payments' );
/* translators: %1$s is 3DS order note title, %2$s is 3DS order note result markup */
$three_d_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$three_d_response_order_note_result_format = '<ul class="ppcp_3ds_result">
<li>%1$s</li>
<li>%2$s</li>
<li>%3$s</li>
<li>%4$s</li>
</ul>';
$three_d_response_order_note_result = sprintf(
$three_d_response_order_note_result_format,
/* translators: %s is liability shift */
sprintf( __( 'Liability Shift: %s', 'woocommerce-paypal-payments' ), esc_html( $result->liability_shift() ) ),
/* translators: %s is enrollment status */
sprintf( __( 'Enrollment Status: %s', 'woocommerce-paypal-payments' ), esc_html( $result->enrollment_status() ) ),
/* translators: %s is authentication status */
sprintf( __( 'Authentication Status: %s', 'woocommerce-paypal-payments' ), esc_html( $result->authentication_result() ) ),
/* translators: %s is card brand */
sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) )
);
$three_d_response_order_note = sprintf(
$three_d_response_order_note_format,
esc_html( $three_d_response_order_note_title ),
wp_kses_post( $three_d_response_order_note_result )
);
$wc_order->add_order_note( $three_d_response_order_note );
$meta_details = array_merge( $result->to_array(), array( 'card_brand' => $card_brand ) );
$wc_order->update_meta_data( PayPalGateway::THREE_D_AUTH_RESULT_META_KEY, $meta_details );
$wc_order->save_meta_data();
/**
* Fired when the 3DS information is added to WC order.
*/
do_action( 'woocommerce_paypal_payments_thee_d_secure_added', $wc_order, $order );
}
}
/**
* Handles the fraud processor response details.
*
* Adds the order note with the fraud processor response details.
* Adds the order meta with the fraud processor response details.
*
* @param FraudProcessorResponse $fraud The fraud processor response (AVS, CVV ...).
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WC order.
*/
protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC_Order $wc_order ): void {
$payment_source = $order->payment_source();
if ( ! $payment_source || $payment_source->name() !== 'card' ) {
return;
}
$fraud_responses = $fraud->to_array();
$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 */
$avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$avs_response_order_note_result_format = '<ul class="ppcp_avs_result">
<li>%1$s</li>
<ul class="ppcp_avs_result_inner">
<li>%2$s</li>
<li>%3$s</li>
</ul>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
/* translators: %s is fraud AVS code */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ),
/* translators: %s is fraud AVS address match */
sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ),
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) )
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
esc_html( $avs_response_order_note_title ),
wp_kses_post( $avs_response_order_note_result )
);
$wc_order->add_order_note( $avs_response_order_note );
$cvv_response_order_note_format = '<ul class="ppcp_cvv_result"><li>%1$s</li></ul>';
$cvv_response_order_note = sprintf(
$cvv_response_order_note_format,
/* translators: %s is fraud CVV match */
sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) )
);
$wc_order->add_order_note( $cvv_response_order_note );
$wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $fraud_responses );
$wc_order->save_meta_data();
/**
* Fired when the fraud result information is added to WC order.
*/
do_action( 'woocommerce_paypal_payments_fraud_result_added', $wc_order, $order );
}
}

View file

@ -112,6 +112,10 @@ trait PaymentsStatusHandlingTrait {
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
/**
* Fired when PayPal order is authorized.
*/
do_action( 'woocommerce_paypal_payments_order_authorized', $wc_order, $authorization );
break;
case AuthorizationStatus::DENIED:
$wc_order->update_status(

View file

@ -11,10 +11,11 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Processor\CreditCardOrderInfoHandlingTrait;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WC_Order;
@ -56,6 +57,8 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
*/
class WCGatewayModule implements ModuleInterface {
use CreditCardOrderInfoHandlingTrait;
/**
* {@inheritDoc}
*/
@ -92,7 +95,7 @@ class WCGatewayModule implements ModuleInterface {
add_action(
'woocommerce_paypal_payments_order_captured',
function ( WC_Order $wc_order, Capture $capture ) {
function ( WC_Order $wc_order, Capture $capture ) use ( $c ) {
$breakdown = $capture->seller_receivable_breakdown();
if ( $breakdown ) {
$wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() );
@ -104,43 +107,34 @@ class WCGatewayModule implements ModuleInterface {
$wc_order->save_meta_data();
}
$order = $c->get( 'session.handler' )->order();
if ( ! $order ) {
return;
}
$fraud = $capture->fraud_processor_response();
if ( $fraud ) {
$fraud_responses = $fraud->to_array();
$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 */
$avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$avs_response_order_note_result_format = '<ul class="ppcp_avs_result">
<li>%1$s</li>
<ul class="ppcp_avs_result_inner">
<li>%2$s</li>
<li>%3$s</li>
</ul>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
/* translators: %s is fraud AVS code */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ),
/* translators: %s is fraud AVS address match */
sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ),
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) )
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
esc_html( $avs_response_order_note_title ),
wp_kses_post( $avs_response_order_note_result )
);
$wc_order->add_order_note( $avs_response_order_note );
$cvv_response_order_note_format = '<ul class="ppcp_cvv_result"><li>%1$s</li></ul>';
$cvv_response_order_note = sprintf(
$cvv_response_order_note_format,
/* translators: %s is fraud CVV match */
sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) )
);
$wc_order->add_order_note( $cvv_response_order_note );
$this->handle_fraud( $fraud, $order, $wc_order );
}
$this->handle_three_d_secure( $order, $wc_order );
},
10,
2
);
add_action(
'woocommerce_paypal_payments_order_authorized',
function ( WC_Order $wc_order, Authorization $authorization ) use ( $c ) {
$order = $c->get( 'session.handler' )->order();
if ( ! $order ) {
return;
}
$fraud = $authorization->fraud_processor_response();
if ( $fraud ) {
$this->handle_fraud( $fraud, $order, $wc_order );
}
$this->handle_three_d_secure( $order, $wc_order );
},
10,
2

View file

@ -11,7 +11,7 @@ class AuthorizationTest extends TestCase
public function testIdAndStatus()
{
$authorizationStatus = \Mockery::mock(AuthorizationStatus::class);
$testee = new Authorization('foo', $authorizationStatus);
$testee = new Authorization('foo', $authorizationStatus, null);
$this->assertEquals('foo', $testee->id());
$this->assertEquals($authorizationStatus, $testee->status());
@ -22,7 +22,7 @@ class AuthorizationTest extends TestCase
$authorizationStatus = \Mockery::mock(AuthorizationStatus::class);
$authorizationStatus->expects('name')->andReturn('CAPTURED');
$testee = new Authorization('foo', $authorizationStatus);
$testee = new Authorization('foo', $authorizationStatus, null);
$expected = [
'id' => 'foo',

View file

@ -17,8 +17,9 @@ class AuthorizationFactoryTest extends TestCase
'id' => 'foo',
'status' => 'CAPTURED',
];
$fraudProcessorResponseFactory = \Mockery::mock(FraudProcessorResponseFactory::class);
$testee = new AuthorizationFactory();
$testee = new AuthorizationFactory($fraudProcessorResponseFactory);
$result = $testee->from_paypal_response($response);
$this->assertInstanceOf(Authorization::class, $result);
@ -36,7 +37,9 @@ class AuthorizationFactoryTest extends TestCase
'status' => 'CAPTURED',
];
$testee = new AuthorizationFactory();
$fraudProcessorResponseFactory = \Mockery::mock(FraudProcessorResponseFactory::class);
$testee = new AuthorizationFactory($fraudProcessorResponseFactory);
$testee->from_paypal_response($response);
}
@ -47,7 +50,9 @@ class AuthorizationFactoryTest extends TestCase
'id' => 'foo',
];
$testee = new AuthorizationFactory();
$fraudProcessorResponseFactory = \Mockery::mock(FraudProcessorResponseFactory::class);
$testee = new AuthorizationFactory($fraudProcessorResponseFactory);
$testee->from_paypal_response($response);
}
}

View file

@ -251,7 +251,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase
}
private function createAuthorization(string $id, string $status): Authorization {
return new Authorization($id, new AuthorizationStatus($status));
return new Authorization($id, new AuthorizationStatus($status), null);
}
private function createCapture(string $id, string $status): Capture {