Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,18 @@ public function capture( Order $order ): Order {

$order = $this->order_factory->from_paypal_response( $json );

$capture_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? null;
$first_capture = $order->purchase_units()[0]->payments()->captures()[0] ?? null;
$capture_status = $first_capture ? $first_capture->status() : null;
if ( $capture_status && $capture_status->is( CaptureStatus::DECLINED ) ) {
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
$fraud = $first_capture->fraud_processor_response();
$decline_message = ( $fraud && $fraud->response_code() )
? sprintf(
/* translators: %s - processor response code and description */
__( 'Payment declined by card processor: %s. Please use a different payment method or contact your bank.', 'woocommerce-paypal-payments' ),
$fraud->get_response_code_message()
)
: __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exact message is duplicated in PaymentsStatusHandlingTrait.php‎, consider adding a helper on the entity (e.g. FraudProcessorResponse::get_customer_decline_message(): string) so in the future requires touching only one place.

throw new RuntimeException( $decline_message );
}

return $order;
Expand Down
95 changes: 90 additions & 5 deletions modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ class FraudProcessorResponse {
*/
protected string $cvv2_code;

/**
* The processor response code (e.g. 9500, 9100).
*
* @var string
*/
protected string $response_code;

/**
* FraudProcessorResponse constructor.
*
* @param string|null $avs_code The AVS response code.
* @param string|null $cvv2_code The CVV response code.
* @param string|null $response_code The processor response code.
*/
public function __construct( ?string $avs_code, ?string $cvv2_code ) {
$this->avs_code = (string) $avs_code;
$this->cvv2_code = (string) $cvv2_code;
public function __construct( ?string $avs_code, ?string $cvv2_code, ?string $response_code = null ) {
$this->avs_code = (string) $avs_code;
$this->cvv2_code = (string) $cvv2_code;
$this->response_code = (string) $response_code;
}

/**
Expand All @@ -57,15 +66,25 @@ public function cvv_code(): string {
return $this->cvv2_code;
}

/**
* Returns the processor response code.
*
* @return string
*/
public function response_code(): string {
return $this->response_code;
}

/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'avs_code' => $this->avs_code(),
'cvv2_code' => $this->cvv_code(),
'avs_code' => $this->avs_code(),
'cvv2_code' => $this->cvv_code(),
'response_code' => $this->response_code(),
);
}

Expand Down Expand Up @@ -155,4 +174,70 @@ public function get_cvv2_code_message(): string {
*/
return $messages[ $this->cvv_code() ] ?? sprintf( '%s: Error', $this->cvv_code() );
}

/**
* Returns the human-readable description for the processor response code.
*
* @return string
*/
public function get_response_code_message(): string {
if ( ! $this->response_code() ) {
return '';
}
$messages = array(
'0000' => '0000: Approved',
'00N7' => '00N7: Decline CVV2 Failure',
'0100' => '0100: Refer to card issuer',
'0390' => '0390: No credit account',
'0500' => '0500: Do not honor',
'0580' => '0580: Transaction not permitted to cardholder',
'0800' => '0800: Bad response reversal amount',
'0880' => '0880: Cryptographic failure',
'0890' => '0890: Unavailable',
'0960' => '0960: System malfunction',
'1000' => '1000: Partial Approval',
'10BR' => '10BR: 3DS Authentication Failure',
'1300' => '1300: Invalid data format',
'1310' => '1310: Invalid amount',
'1312' => '1312: Invalid transaction card issuer acquirer',
'1317' => '1317: Invalid capture date',
'1320' => '1320: Invalid currency code',
'1330' => '1330: Invalid account',
'1335' => '1335: Invalid account type',
'1340' => '1340: Invalid terminal id',
'1350' => '1350: Invalid merchant/terminal city',
'1360' => '1360: Bad or malformed request',
'1370' => '1370: Issuer unavailable',
'1380' => '1380: Updates not allowed',
'1382' => '1382: Bad CVV2',
'1384' => '1384: Similar transaction recently submitted',
'1390' => '1390: Trace number error',
'1393' => '1393: Transaction amount range error',
'5100' => '5100: Generic Decline',
'5110' => '5110: CVV2 Failure',
'5120' => '5120: Insufficient funds',
'5130' => '5130: Invalid PIN',
'5140' => '5140: Card closed',
'5150' => '5150: Pick up card (fraud)',
'5160' => '5160: Unauthorized user',
'5170' => '5170: Card blocked',
'5180' => '5180: Declined by the issuer',
'5200' => '5200: Account closed',
'5400' => '5400: Expired card',
'5910' => '5910: Issuer not available, return to issuer',
'5920' => '5920: Issuer not available, return to issuer',
'5930' => '5930: Card not activated',
'6300' => '6300: Account blocked',
'9100' => '9100: Declined, Please Retry',
'9500' => '9500: Suspected Fraud',
'9510' => '9510: Security Violation',
'9520' => '9520: Lost or Stolen Card',
'9530' => '9530: Hold - Call issuer',
'9540' => '9540: Refused Card',
'9600' => '9600: Unacceptable PIN - Transaction Declined - Retry',
'PCNF' => 'PCNF: Purchase Confirmation Not Received',
'PCOM' => 'PCOM: Purchase Confirmation Received',
);
return $messages[ $this->response_code() ] ?? sprintf( '%s: Unknown response code', $this->response_code() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class FraudProcessorResponseFactory {
* @return FraudProcessorResponse
*/
public function from_paypal_response( stdClass $data ): FraudProcessorResponse {
$avs_code = ( $data->avs_code ?? null ) ?: null;
$cvv_code = ( $data->cvv_code ?? null ) ?: null;
$avs_code = ( $data->avs_code ?? null ) ?: null;
$cvv_code = ( $data->cvv_code ?? null ) ?: null;
$response_code = ( $data->response_code ?? null ) ?: null;

return new FraudProcessorResponse( $avs_code, $cvv_code );
return new FraudProcessorResponse( $avs_code, $cvv_code, $response_code );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,18 @@ protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC
<li>%1$s</li>
<li>%2$s</li>
<li>%3$s</li>
%4$s
</ul>';
$response_code_item = $fraud->response_code()
? sprintf(
'<li>%s</li>',
sprintf(
/* translators: %s is processor response code and description */
esc_html__( 'Response Code: %s', 'woocommerce-paypal-payments' ),
esc_html( $fraud->get_response_code_message() )
)
)
: '';
$response_order_note_result = sprintf(
$response_order_note_result_format,
/* translators: %1$s is card brand and %2$s card last 4 digits */
Expand All @@ -115,6 +126,7 @@ protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), $fraud->get_avs_code_message() ),
/* translators: %s is fraud CVV message */
sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_message() ),
$response_code_item,
);
$response_order_note = sprintf(
$response_order_note_format,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,28 @@ protected function handle_capture_status(
// It is checked in the capture endpoint already, but there are other ways to capture,
// such as when paid via saved card.
case CaptureStatus::DECLINED:
$fraud = $capture->fraud_processor_response();
if ( $fraud && $fraud->response_code() ) {
$wc_order->add_order_note(
sprintf(
/* translators: %s - processor response code and description */
__( 'PayPal payment declined. Processor response: %s', 'woocommerce-paypal-payments' ),
$fraud->get_response_code_message()
)
);
}
$wc_order->update_status(
'failed',
__( 'Could not capture the payment.', 'woocommerce-paypal-payments' )
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code calls add_order_note and then update_status which also writes a status-change note, consider passing the $note argument of update_status() instead, e.g.:

$wc_order->update_status(
    'failed',
    $fraud && $fraud->response_code()
        ? sprintf( __( 'Could not capture the payment. Processor response: %s', '' ), $fraud->get_response_code_message() )
        : __( 'Could not capture the payment.', '' )
);

throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
$decline_message = ( $fraud && $fraud->response_code() )
? sprintf(
/* translators: %s - processor response code and description */
__( 'Payment declined by card processor: %s. Please use a different payment method or contact your bank.', 'woocommerce-paypal-payments' ),
$fraud->get_response_code_message()
)
: __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' );
throw new RuntimeException( $decline_message );
case CaptureStatus::PENDING:
case CaptureStatus::FAILED:
$wc_order->update_status(
Expand Down
25 changes: 25 additions & 0 deletions modules/ppcp-wc-gateway/src/WCGatewayModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\FraudProcessorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
Expand Down Expand Up @@ -166,6 +167,30 @@ function ( int $order_id ) use ( $fees_renderer ) {
}
);

add_action(
'woocommerce_admin_order_totals_after_total',
function ( int $order_id ) {
$wc_order = wc_get_order( $order_id );
if ( ! $wc_order instanceof WC_Order ) {
return;
}
$fraud_result = $wc_order->get_meta( PayPalGateway::FRAUD_RESULT_META_KEY );
if ( empty( $fraud_result['response_code'] ) ) {
return;
}
$fraud = new FraudProcessorResponse(
$fraud_result['avs_code'] ?? null,
$fraud_result['cvv2_code'] ?? null,
$fraud_result['response_code']
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it instantiates a full FraudProcessorResponse from stored meta only to call get_response_code_message() below. The avs/cvv args are not used for output here, consider reusing FraudProcessorResponseFactory::from_paypal_response( (object) $fraud_result ) instead.

printf(
'<tr><td class="label">%s:</td><td width="1%%"></td><td class="total">%s</td></tr>',
esc_html__( 'Processor Response', 'woocommerce-paypal-payments' ),
esc_html( $fraud->get_response_code_message() )
);
}
);

add_action(
'admin_enqueue_scripts',
function () use ( $c ) {
Expand Down
Loading
Loading