Skip to content

Commit 9bcf822

Browse files
authored
Merge pull request #4314 from woocommerce/PCP-6061-enhanced-order-decline-reason-detection-via-processor-response-object-integration
Enhanced order decline reason detection via processor response object integration (6061)
2 parents 5ff420b + e2d0726 commit 9bcf822

10 files changed

Lines changed: 542 additions & 15 deletions

File tree

modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,14 @@ public function capture( Order $order ): Order {
345345

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

348-
$capture_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? null;
348+
$first_capture = $order->purchase_units()[0]->payments()->captures()[0] ?? null;
349+
$capture_status = $first_capture ? $first_capture->status() : null;
349350
if ( $capture_status && $capture_status->is( CaptureStatus::DECLINED ) ) {
350-
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
351+
$fraud = $first_capture->fraud_processor_response();
352+
$decline_message = $fraud
353+
? $fraud->get_customer_decline_message()
354+
: __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' );
355+
throw new RuntimeException( $decline_message );
351356
}
352357

353358
return $order;

modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ class FraudProcessorResponse {
2828
*/
2929
protected string $cvv2_code;
3030

31+
/**
32+
* The processor response code (e.g. 9500, 9100).
33+
*
34+
* @var string
35+
*/
36+
protected string $response_code;
37+
3138
/**
3239
* FraudProcessorResponse constructor.
3340
*
3441
* @param string|null $avs_code The AVS response code.
3542
* @param string|null $cvv2_code The CVV response code.
43+
* @param string|null $response_code The processor response code.
3644
*/
37-
public function __construct( ?string $avs_code, ?string $cvv2_code ) {
38-
$this->avs_code = (string) $avs_code;
39-
$this->cvv2_code = (string) $cvv2_code;
45+
public function __construct( ?string $avs_code, ?string $cvv2_code, ?string $response_code = null ) {
46+
$this->avs_code = (string) $avs_code;
47+
$this->cvv2_code = (string) $cvv2_code;
48+
$this->response_code = (string) $response_code;
4049
}
4150

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

69+
/**
70+
* Returns the processor response code.
71+
*
72+
* @return string
73+
*/
74+
public function response_code(): string {
75+
return $this->response_code;
76+
}
77+
6078
/**
6179
* Returns the object as array.
6280
*
6381
* @return array
6482
*/
6583
public function to_array(): array {
6684
return array(
67-
'avs_code' => $this->avs_code(),
68-
'cvv2_code' => $this->cvv_code(),
85+
'avs_code' => $this->avs_code(),
86+
'cvv2_code' => $this->cvv_code(),
87+
'response_code' => $this->response_code(),
6988
);
7089
}
7190

@@ -155,4 +174,87 @@ public function get_cvv2_code_message(): string {
155174
*/
156175
return $messages[ $this->cvv_code() ] ?? sprintf( '%s: Error', $this->cvv_code() );
157176
}
177+
178+
/**
179+
* Returns the customer-facing decline message, including the processor response code when available.
180+
*
181+
* @return string
182+
*/
183+
public function get_customer_decline_message(): string {
184+
if ( $this->response_code() ) {
185+
return sprintf(
186+
/* translators: %s - processor response code and description */
187+
__( 'Payment declined by card processor: %s. Please use a different payment method or contact your bank.', 'woocommerce-paypal-payments' ),
188+
$this->get_response_code_message()
189+
);
190+
}
191+
192+
return __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' );
193+
}
194+
195+
/**
196+
* Returns the human-readable description for the processor response code.
197+
*
198+
* @return string
199+
*/
200+
public function get_response_code_message(): string {
201+
if ( ! $this->response_code() ) {
202+
return '';
203+
}
204+
$messages = array(
205+
'0000' => '0000: Approved',
206+
'00N7' => '00N7: Decline CVV2 Failure',
207+
'0100' => '0100: Refer to card issuer',
208+
'0390' => '0390: No credit account',
209+
'0500' => '0500: Do not honor',
210+
'0580' => '0580: Transaction not permitted to cardholder',
211+
'0800' => '0800: Bad response reversal amount',
212+
'0880' => '0880: Cryptographic failure',
213+
'0890' => '0890: Unavailable',
214+
'0960' => '0960: System malfunction',
215+
'1000' => '1000: Partial Approval',
216+
'10BR' => '10BR: 3DS Authentication Failure',
217+
'1300' => '1300: Invalid data format',
218+
'1310' => '1310: Invalid amount',
219+
'1312' => '1312: Invalid transaction card issuer acquirer',
220+
'1317' => '1317: Invalid capture date',
221+
'1320' => '1320: Invalid currency code',
222+
'1330' => '1330: Invalid account',
223+
'1335' => '1335: Invalid account type',
224+
'1340' => '1340: Invalid terminal id',
225+
'1350' => '1350: Invalid merchant/terminal city',
226+
'1360' => '1360: Bad or malformed request',
227+
'1370' => '1370: Issuer unavailable',
228+
'1380' => '1380: Updates not allowed',
229+
'1382' => '1382: Bad CVV2',
230+
'1384' => '1384: Similar transaction recently submitted',
231+
'1390' => '1390: Trace number error',
232+
'1393' => '1393: Transaction amount range error',
233+
'5100' => '5100: Generic Decline',
234+
'5110' => '5110: CVV2 Failure',
235+
'5120' => '5120: Insufficient funds',
236+
'5130' => '5130: Invalid PIN',
237+
'5140' => '5140: Card closed',
238+
'5150' => '5150: Pick up card (fraud)',
239+
'5160' => '5160: Unauthorized user',
240+
'5170' => '5170: Card blocked',
241+
'5180' => '5180: Declined by the issuer',
242+
'5200' => '5200: Account closed',
243+
'5400' => '5400: Expired card',
244+
'5910' => '5910: Issuer not available, return to issuer',
245+
'5920' => '5920: Issuer not available, return to issuer',
246+
'5930' => '5930: Card not activated',
247+
'6300' => '6300: Account blocked',
248+
'9100' => '9100: Declined, Please Retry',
249+
'9500' => '9500: Suspected Fraud',
250+
'9510' => '9510: Security Violation',
251+
'9520' => '9520: Lost or Stolen Card',
252+
'9530' => '9530: Hold - Call issuer',
253+
'9540' => '9540: Refused Card',
254+
'9600' => '9600: Unacceptable PIN - Transaction Declined - Retry',
255+
'PCNF' => 'PCNF: Purchase Confirmation Not Received',
256+
'PCOM' => 'PCOM: Purchase Confirmation Received',
257+
);
258+
return $messages[ $this->response_code() ] ?? sprintf( '%s: Unknown response code', $this->response_code() );
259+
}
158260
}

modules/ppcp-api-client/src/Factory/FraudProcessorResponseFactory.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ class FraudProcessorResponseFactory {
2525
* @return FraudProcessorResponse
2626
*/
2727
public function from_paypal_response( stdClass $data ): FraudProcessorResponse {
28-
$avs_code = ( $data->avs_code ?? null ) ?: null;
29-
$cvv_code = ( $data->cvv_code ?? null ) ?: null;
28+
$avs_code = ( $data->avs_code ?? null ) ?: null;
29+
$cvv_code = ( $data->cvv_code ?? null ) ?: null;
30+
$response_code = ( $data->response_code ?? null ) ?: null;
3031

31-
return new FraudProcessorResponse( $avs_code, $cvv_code );
32+
return new FraudProcessorResponse( $avs_code, $cvv_code, $response_code );
3233
}
3334
}

modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,18 @@ protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC
106106
<li>%1$s</li>
107107
<li>%2$s</li>
108108
<li>%3$s</li>
109+
%4$s
109110
</ul>';
111+
$response_code_item = $fraud->response_code()
112+
? sprintf(
113+
'<li>%s</li>',
114+
sprintf(
115+
/* translators: %s is processor response code and description */
116+
esc_html__( 'Response Code: %s', 'woocommerce-paypal-payments' ),
117+
esc_html( $fraud->get_response_code_message() )
118+
)
119+
)
120+
: '';
110121
$response_order_note_result = sprintf(
111122
$response_order_note_result_format,
112123
/* translators: %1$s is card brand and %2$s card last 4 digits */
@@ -115,6 +126,7 @@ protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC
115126
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), $fraud->get_avs_code_message() ),
116127
/* translators: %s is fraud CVV message */
117128
sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_message() ),
129+
$response_code_item,
118130
);
119131
$response_order_note = sprintf(
120132
$response_order_note_format,

modules/ppcp-wc-gateway/src/Processor/PaymentsStatusHandlingTrait.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,19 @@ protected function handle_capture_status(
8787
// It is checked in the capture endpoint already, but there are other ways to capture,
8888
// such as when paid via saved card.
8989
case CaptureStatus::DECLINED:
90-
$wc_order->update_status(
91-
'failed',
92-
__( 'Could not capture the payment.', 'woocommerce-paypal-payments' )
93-
);
94-
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
90+
$fraud = $capture->fraud_processor_response();
91+
$status_note = ( $fraud && $fraud->response_code() )
92+
? sprintf(
93+
/* translators: %s - processor response code and description */
94+
__( 'Could not capture the payment. Processor response: %s', 'woocommerce-paypal-payments' ),
95+
$fraud->get_response_code_message()
96+
)
97+
: __( 'Could not capture the payment.', 'woocommerce-paypal-payments' );
98+
$wc_order->update_status( 'failed', $status_note );
99+
$decline_message = $fraud
100+
? $fraud->get_customer_decline_message()
101+
: __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' );
102+
throw new RuntimeException( $decline_message );
95103
case CaptureStatus::PENDING:
96104
case CaptureStatus::FAILED:
97105
$wc_order->update_status(

modules/ppcp-wc-gateway/src/WCGatewayModule.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,27 @@ function ( int $order_id ) use ( $fees_renderer ) {
165165
}
166166
);
167167

168+
add_action(
169+
'woocommerce_admin_order_totals_after_total',
170+
function ( int $order_id ) use ( $c ) {
171+
$wc_order = wc_get_order( $order_id );
172+
if ( ! $wc_order instanceof WC_Order ) {
173+
return;
174+
}
175+
$fraud_result = $wc_order->get_meta( PayPalGateway::FRAUD_RESULT_META_KEY );
176+
if ( empty( $fraud_result['response_code'] ) ) {
177+
return;
178+
}
179+
$fraud = $c->get( 'api.factory.fraud-processor-response' )
180+
->from_paypal_response( (object) $fraud_result );
181+
printf(
182+
'<tr><td class="label">%s:</td><td width="1%%"></td><td class="total">%s</td></tr>',
183+
esc_html__( 'Processor Response', 'woocommerce-paypal-payments' ),
184+
esc_html( $fraud->get_response_code_message() )
185+
);
186+
}
187+
);
188+
168189
add_action(
169190
'admin_enqueue_scripts',
170191
function () use ( $c ) {

0 commit comments

Comments
 (0)