feat(checkout): verify the approved PayPal order and its capture against the order

verifyApprovedOrder accepts PayPal's order only if it is APPROVED, its
first purchase unit's custom_id is the order's uuid, and its amount is the
order total in EUR. verifyCompletedCapture accepts the order after the
capture only if it and its first capture are COMPLETED for the order
total in EUR, and returns the capture id. Both read PayPal's Orders v2
JSON in snake_case, as PayPalApi parses the response body.

Money is compared in integer cents: PayPal's "12.34" is parsed strictly,
the order total is Math.round(total * 100). For every total up to
3000.00 the amount the checkout sends (total.toFixed(2)) parses to the
cents the capture compares.

A rejection reason repeats only PayPal's statuses, currency codes and
amounts, never the payer's or the shipping data of the response.

Refs libretech/mp#71
This commit is contained in:
libretech committed 2026-10-09 01:12:20 +02:00
1 parent 2bbffeed2d
commit 4d77998b51
4 files changed
+577

No files matched your search

@@ -0,0 +1,88 @@
{
"id": "5O190127TN364715T",
"intent": "CAPTURE",
"status": "APPROVED",
"payment_source": {
"paypal": {
"email_address": "erika.buyer@example.org",
"account_id": "QYR5Z8XDVJNXQ",
"account_status": "VERIFIED",
"name": {
"given_name": "Erika",
"surname": "Mustermann"
},
"address": {
"country_code": "DE"
}
}
},
"purchase_units": [
{
"reference_id": "11111111-2222-4333-8444-555555555555",
"amount": {
"currency_code": "EUR",
"value": "22.85",
"breakdown": {
"item_total": {
"currency_code": "EUR",
"value": "15.04"
},
"shipping": {
"currency_code": "EUR",
"value": "4.95"
},
"tax_total": {
"currency_code": "EUR",
"value": "2.86"
}
}
},
"payee": {
"email_address": "merchant@example.com",
"merchant_id": "7KNGBPH2U58GQ"
},
"custom_id": "11111111-2222-4333-8444-555555555555",
"invoice_id": "R-12-4711",
"shipping": {
"name": {
"full_name": "Erika Mustermann"
},
"address": {
"address_line_1": "Musterstraße 1",
"admin_area_2": "Stuttgart",
"postal_code": "70190",
"country_code": "DE"
}
}
}
],
"payer": {
"name": {
"given_name": "Erika",
"surname": "Mustermann"
},
"email_address": "erika.buyer@example.org",
"payer_id": "QYR5Z8XDVJNXQ",
"address": {
"country_code": "DE"
}
},
"create_time": "2026-10-08T19:12:40Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/5O190127TN364715T",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/5O190127TN364715T",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/5O190127TN364715T/capture",
"rel": "capture",
"method": "POST"
}
]
}