diff --git a/composer.lock b/composer.lock index 450c96e96..afd6af3dc 100644 --- a/composer.lock +++ b/composer.lock @@ -4996,5 +4996,5 @@ "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 346002474..7f8c7acc2 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -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(); diff --git a/modules/ppcp-api-client/src/Entity/Authorization.php b/modules/ppcp-api-client/src/Entity/Authorization.php index ae8e8f591..007e12475 100644 --- a/modules/ppcp-api-client/src/Entity/Authorization.php +++ b/modules/ppcp-api-client/src/Entity/Authorization.php @@ -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; } } diff --git a/modules/ppcp-api-client/src/Factory/AuthorizationFactory.php b/modules/ppcp-api-client/src/Factory/AuthorizationFactory.php index c65e764af..d16aee2fb 100644 --- a/modules/ppcp-api-client/src/Factory/AuthorizationFactory.php +++ b/modules/ppcp-api-client/src/Factory/AuthorizationFactory.php @@ -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 ); } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 470ddef1c..8220b0a5b 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -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. diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php new file mode 100644 index 000000000..71e96b90a --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -0,0 +1,147 @@ +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 = ''; + $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 = ''; + $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 = ''; + $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 ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/PaymentsStatusHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/PaymentsStatusHandlingTrait.php index e1f4f9654..e6186badf 100644 --- a/modules/ppcp-wc-gateway/src/Processor/PaymentsStatusHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/PaymentsStatusHandlingTrait.php @@ -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( diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 84ea0336b..3dfd70810 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -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 = ''; - $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 = ''; - $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 diff --git a/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php b/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php index 8843ce002..c5895a2f4 100644 --- a/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php +++ b/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php @@ -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', diff --git a/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php index 12a76a016..71df98376 100644 --- a/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php @@ -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); } } diff --git a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php index f65338f67..5dd7c588f 100644 --- a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php @@ -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 {