Commit Graph
7 Commits
Author SHA1 Message Date
libretech c7a422621a feat(order): name the fields an order still lacks for a checkout
missingCheckoutFields returns which of email, invoiceAddress and
delivery an order lacks, so the checkout can answer 400 before it
creates a PayPal order.

Refs libretech/mp#71
2026-10-09 01:12:21 +02:00
libretech d56ae43597 feat(checkout): capture a payment through injected dependencies
capturePayment(deps, { uuid, paypalOrderId, now }) is the capture flow
without Strapi or the PayPal SDK, so it is tested with fakes:

- a missing order is answered 404;
- a paid order is answered alreadyCaptured for the PayPal order it was
  paid with, else 409, without a call or a write;
- a PayPal order the checkout did not store on the order is answered
  409 before PayPal is asked anything;
- a PayPal order that does not verify as approved for the order is
  answered 409 without a capture;
- the capture is sent with the PayPal-Request-Id
  capture-<uuid>-<paypalOrderId>, so a retry captures once;
- a capture that does not verify leaves the order unpaid, is logged as
  an error with the order id and the reason, and is answered 502: PayPal
  may have taken the money, so it needs a manual refund check;
- the payment is written only while the order still carries the PayPal
  order id, so an order whose cart or address changed during the
  capture stays unpaid (409, logged with the capture id);
- PayPal and network errors are answered 502 and logged by name,
  message and status code only.

Refs libretech/mp#71
2026-10-09 01:12:20 +02:00
libretech f599b2c84b feat(logging): describe an error by its name, message and status code
describeError keeps an error's name, the first line of its message, cut
to 200 characters, and its HTTP status code. An error of the PayPal SDK
or of axios carries its request, whose Authorization header holds the
PayPal client credentials or an access token, so the error object itself
must not reach the log. The SDK's response validation error quotes the
response values on the lines after the first.

Refs libretech/mp#71
2026-10-09 01:12:20 +02:00
libretech 4d77998b51 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
2026-10-09 01:12:20 +02:00
libretech 88f4ee75d7 feat(order): lock an order for the customer once its payment is authorised
Refs libretech/mp#71
2026-10-08 22:54:59 +02:00
libretech b9670b7229 feat(order): parse the customer's order update against the checkout fields
parseCustomerUpdate accepts { data } with a subset of the seven fields the
shop's checkout sends: email, acceptedTermsAndConditionsAt, invoiceAddress,
deliveryAddress, invoiceAddressStructured, deliveryAddressStructured and
delivery. Any other key, at the top level, in data or in a structured
address, rejects the whole body, and the errors name the fields without
repeating their values.

The terms are recorded at the server's time whenever the client sends a
true value: the invoice is dated by it.

Refs libretech/mp#71
2026-10-08 22:54:59 +02:00
libretech 9b0c6af8b7 Test the checkout code; publish images only for releases (#5)
build / test (push) Successful in 7s
build / build (push) Skipped
- src/checkout/ (PayPal order request, pricing, VAT) and src/logging/format.ts
  hold the checkout's pure code, moved out of PayPalApi, the product service
  and config without a behaviour change.
- npm test runs node:test on the TypeScript sources (type stripping), with no
  dependencies: 21 tests, 13 pin today's output, 8 describe the correct
  behaviour for the known defects of libretech/mp#67
  and are marked todo.
- CI: the tests run in the image's base (node:22.14.0-alpine) for every pull
  request, push to main and release tag. The image is built for pull requests
  and built and published only for v* tags; main no longer publishes :main
  and :sha-* images.

Refs libretech/mp#71

Co-authored-by: Michael Czechowski <mail@librete.ch>
2026-10-08 22:36:47 +02:00