Brief setup:
Testing with sandbox credentials and testing against a direct credit/debit card number. I will note I have modified the plugin to use the Advanced checkout rather than Standard checkout (so I can use hosted fields and 3D secure).
Problem:
The problem is that even if the test is set up to reject the card (for example using 'CCREJECT-SF' in the name) the transaction still goes through sucessfully in nopcommerce and is marked as Paid. The response from paypal does show it is declined.
Investigation:
See https://developer.paypal.com/tools/sandbox/card-testing/ for other decline triggers and another sample response.
My test response
{
"create_time": "2023-04-04T17:39:18Z",
"id": "5T885433KX1697344",
"intent": "CAPTURE",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/5T885433KX1697344",
"method": "GET",
"rel": "self"
}
],
"payment_source": {
"card": {
"brand": "VISA",
"last_digits": "7704",
"type": "CREDIT"
}
},
"purchase_units": [
{
"amount": {
"breakdown": {
"discount": {
"currency_code": "xxxxxx",
"value": "xxxxxx"
},
"handling": {
"currency_code": "AUD",
"value": "0.00"
},
"insurance": {
"currency_code": "AUD",
"value": "0.00"
},
"item_total": {
"currency_code": "AUD",
"value": "245.00"
},
"shipping": {
"currency_code": "AUD",
"value": "0.00"
},
"shipping_discount": {
"currency_code": "AUD",
"value": "0.00"
},
"tax_total": {
"currency_code": "AUD",
"value": "0.00"
}
},
"currency_code": "AUD",
"value": "245.00"
},
"custom_id": "e9e1bdeb-39f3-4294-9065-802c4a602bfd",
"description": "Purchase at 'Your store name'",
"items": [
{
"description": "HTC - One (M8) 4G LTE Cell Phone with 32GB Memory - Gunmetal (Sprint)",
"name": "HTC One M8 Android L 5.0 Lollipop",
"quantity": "1",
"sku": "M8_HTC_5L",
"tax": {
"currency_code": "AUD",
"value": "0.00"
},
"unit_amount": {
"currency_code": "AUD",
"value": "245.00"
}
},
{
"description": "Gift wrapping - No",
"name": "Gift wrapping",
"quantity": "1",
"tax": {
"currency_code": "AUD",
"value": "0.00"
},
"unit_amount": {
"currency_code": "AUD",
"value": "0.00"
}
}
],
"payee": {
"email_address": "[email protected]",
"merchant_id": "DRA95KX5SXBGJ"
},
"payments": {
"captures": [
{
"amount": {
"currency_code": "AUD",
"value": "245.00"
},
"create_time": "2023-04-04T17:50:40Z",
"custom_id": "xxxxxx",
"final_capture": true,
"id": "80450137DK344141S",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/payments/captures/80450137DK344141S",
"method": "GET",
"rel": "self"
},
{
"href": "https://api.sandbox.paypal.com/v2/payments/captures/80450137DK344141S/refund",
"method": "POST",
"rel": "refund"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/5T885433KX1697344",
"method": "GET",
"rel": "up"
}
],
"processor_response": {
"avs_code": "G",
"cvv_code": "P",
"response_code": "9500"
},
"seller_protection": {
"status": "NOT_ELIGIBLE"
},
"seller_receivable_breakdown": {
"gross_amount": {
"currency_code": "AUD",
"value": "245.00"
},
"net_amount": {
"currency_code": "AUD",
"value": "238.82"
},
"paypal_fee": {
"currency_code": "AUD",
"value": "6.18"
}
},
"status": "DECLINED",
"update_time": "2023-04-04T17:50:40Z"
}
]
},
"reference_id": "e9e1bdeb-39f3-4294-9065-802c4a602bfd",
"shipping": {
"address": {
"address_line_1": "*****",
"address_line_2": "*****",
"admin_area_1": "XX",
"admin_area_2": "Kalgoorlie",
"country_code": "AU",
"postal_code": "6430"
},
"name": {
"full_name": "*****"
}
},
"soft_descriptor": "Your store name"
}
],
"status": "COMPLETED",
"update_time": "2023-04-04T17:39:18Z"
}
Notice the value of 'status' under captures (DECLINED). Also notice the response_Code under processor_response. The HTTP response code is 201 Created and there are no errors
From PayPalCommercePaymentMethod.cs in the ProcessPaymentAsync function we have the following
//authorize or capture the order
var (order, error) = _settings.PaymentType == PaymentType.Capture
? await _serviceManager.CaptureAsync(_settings, orderId.ToString())
: (_settings.PaymentType == PaymentType.Authorize
? await _serviceManager.AuthorizeAsync(_settings, orderId.ToString())
: (default, default));
if (!string.IsNullOrEmpty(error))
return new ProcessPaymentResult { Errors = new[] { error } };
//request succeeded
var result = new ProcessPaymentResult();
var purchaseUnit = order.PurchaseUnits
.FirstOrDefault(item => item.ReferenceId.Equals(processPaymentRequest.OrderGuid.ToString()));
var authorization = purchaseUnit.Payments?.Authorizations?.FirstOrDefault();
if (authorization != null)
{
result.AuthorizationTransactionId = authorization.Id;
result.AuthorizationTransactionResult = authorization.Status;
result.NewPaymentStatus = PaymentStatus.Authorized;
}
var capture = purchaseUnit.Payments?.Captures?.FirstOrDefault();
if (capture != null)
{
result.CaptureTransactionId = capture.Id;
result.CaptureTransactionResult = capture.Status;
result.NewPaymentStatus = PaymentStatus.Paid;
}
Given the response of 201 Created and there are no error fields it continues and without checking the capture status it marks it as paid
The end result is it appears that irrespective of whether or not the card is successful the order is created and marked as paid which is clearly wrong