paypalOrderId is the PayPal order the checkout created for the order,
paypalCaptureId and paymentCapturedAt record the verified capture. All
three are private, so the content API never returns them. Strapi adds the
columns when it syncs the schema at startup.
types/generated/contentTypes.d.ts is the output of strapi
ts:generate-types, run in the release image without network.
Refs libretech/mp#71
PUT /orders/:uuid/cart passed the body through sanitize.contentAPI.input,
which keeps every public writable attribute, to the order update. The shop
forwards the browser's body unchanged, so a browser could set
paymentAuthorised on its own order, and the order lifecycle then generated
and mailed the invoice.
The handler now parses the body with parseCustomerUpdate and writes only
the fields it returns, plus the customer it finds or creates from the
invoice address. A body with any other field is answered 400 with the
error list, and the warning it logs names the fields, not their values.
A delivery method that does not exist is answered 400.
Once the payment is authorised, the order is locked for the customer:
the update, add-product and remove-product answer 409.
Refs libretech/mp#71
findOne and findOneByUuid mapped order.cart before checking that the order
exists, and the service's findOneByUuid did the same, so a missing order
threw a TypeError: GET /orders/:uuid/cart, add-product, remove-product,
generate-invoice and generate-delivery-note answered 500.
The service's findOneByUuid now returns null for a missing order, and each
caller handles it: the controllers answer 404, sendInvoice and
sendDeliveryNote throw a NotFoundError.
Refs libretech/mp#71
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
- 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>
Recovered work in progress from 2026-05. A product-cover can be marked
standalone, and a standalone product (Journaling Booklet) prices as the
cover price without the pattern/pages/ruling lookups, which do not exist
for it. The remaining component lookups are now guarded so a missing
relation yields null instead of calling findOne with undefined.