The shop side of the checkout fix in libretech/mp#71 (step 7). Not to be merged before the CMS side is released.
The order update proxy forwards only the checkout's 7 fields to the CMS; any other field is answered 400 here.
The order routes pass the CMS's status and a safe message to the browser instead of a bare "Server Error".
Each checkout step shows a German message when it fails; when PayPal may already have taken money, it says not to pay again.
E-mail field is type=email; a repeated capture (alreadyCaptured) counts as a successful payment.
The contact form posts to the mail service's /v1/send/message with its real payload (it posted to /send, which does not exist).
CI: tests for every change; the image is built for pull requests and published only for release tags.
Checked: 51 unit tests pass on Node 24 and Node 22. This PR's CI build is the first nuxt build of these changes.
The shop side of the checkout fix in https://git.librete.ch/libretech/mp/issues/71 (step 7). Not to be merged before the CMS side is released.
- The order update proxy forwards only the checkout's 7 fields to the CMS; any other field is answered 400 here.
- The order routes pass the CMS's status and a safe message to the browser instead of a bare "Server Error".
- Each checkout step shows a German message when it fails; when PayPal may already have taken money, it says not to pay again.
- E-mail field is type=email; a repeated capture (alreadyCaptured) counts as a successful payment.
- The contact form posts to the mail service's /v1/send/message with its real payload (it posted to /send, which does not exist).
- CI: tests for every change; the image is built for pull requests and published only for release tags.
Checked: 51 unit tests pass on Node 24 and Node 22. This PR's CI build is the first nuxt build of these changes.
The order routes (get, put, add-product, remove-product, checkout, capture)
call forwardToCms, which throws a CMS error on as
createError({ statusCode, statusMessage, data: { message, errors?, missing? } }),
built by the pure shopErrorFromCms (server/utils/cmsError.ts).
Before, the FetchError was thrown on as it was: the browser got the CMS's
status, but Nitro treated it as unhandled, answered "Server Error" without
data and logged every CMS 4xx as [unhandled]. Now the browser also gets the
CMS's message, the errors of a rejected update and the fields a checkout
misses, and no other field. A status that is the shop's own fault (401, 403,
...) is answered 500, a CMS that does not answer 503; 5xx are logged without
the query and the order uuid.
npm test runs tests/unit with Node's type stripping and no dependencies, as
in libreshop/cms. nuxt.config keeps tests/ out of the app's type check.
Refs libretech/mp#71
PUT /api/orders/:uuid forwarded the browser's body to the CMS unchanged. It
now forwards { data } with only the seven fields of the checkout's steps:
email, acceptedTermsAndConditionsAt, invoiceAddress, deliveryAddress,
invoiceAddressStructured, deliveryAddressStructured and delivery. Any other
field, a field beside data, or a body of another shape is answered 400
"Invalid order update" with the CMS's error format, without calling the
CMS. The values are left to the CMS, which checks them and stays the
authority; this is defence in depth.
pickCustomerUpdate (server/utils/customerUpdate.ts) is pure and tested with
the exact payloads of steps 1 and 2 and with every server-only attribute of
the order.
Refs libretech/mp#71
checkoutErrorMessage (utils/checkoutError.ts) maps the status and body of a
failed request, the shop's answer or a raw CMS (Strapi) error, to one fixed
message for the customer, addressed with "du": an invalid e-mail address
(when data.email is among the rejected fields), other invalid input, an order
already paid, a product no longer available, an order not ready for checkout
(naming the missing steps), a payment that does not fit the order or an order
changed during the payment ("Bitte starte die Zahlung neu"), PayPal not
reachable ("versuche es in ein paar Minuten noch einmal"), a payment PayPal
may have taken that the CMS could not confirm or record ("Bitte bezahle nicht
noch einmal"), and a general fallback. It never shows the server's text.
Steps 1 and 2 show the message above their submit button, in the style of
step 3's payment error, instead of logging the error only. Step 3 shows it
for a failed PayPal order creation or capture. The e-mail input of step 1 is
type="email" (Input.vue takes a type).
Refs libretech/mp#71
The CMS answers a capture of an order already paid with this PayPal order
with { success: true, alreadyCaptured: true } instead of the order. Step 3
took that answer for a capture without authorisation and showed a payment
error. It now reloads the paid order (keeping the shown one if the reload
fails) and continues as after a first capture: confirmation, the
checkout-payment-completed event, and the redirect to the order's result
page. capturePayment is typed as Order | AlreadyCaptured, and the pure
isAlreadyCaptured (utils/captureResponse.ts) tells them apart.
Refs libretech/mp#71
POST /api/contact posted { to, subject, body, replyTo } to ${mailApiUrl}/send,
which the mail service (libreshop/mail src/app.py) does not have, so every
message failed with a 500. It now posts { to_email, subject, message } to
POST /v1/send/message, still to paperwork@muellerprints.de, with the same
subject and text as before. The service sets no Reply-To; the sender's
address stays in the text. A line break in the subject is replaced by a
space, so it cannot start another mail header.
mailApiUrl was read from runtimeConfig without being declared there, so
NUXT_MAIL_API_URL could not set it and the fallback http://mail:2222 was
always used. It is declared now, with that default; mp's compose.yml sets
no NUXT_MAIL_API_URL.
contactMail and mailSendUrl (server/utils/contactMail.ts) are pure and tested.
Refs libretech/mp#71
As libreshop/cms adopted: a test job pipes the source into the image's base
(the Dockerfile's first FROM, node:22-slim) and runs npm test there without
network. The image build needs it and runs for pull requests (build only)
and v* tags (published if PUBLISH_ENABLED). main no longer publishes :main
and :sha-* images; mp pins the shop image by version tag.
Refs libretech/mp#71
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The shop side of the checkout fix in libretech/mp#71 (step 7). Not to be merged before the CMS side is released.
Checked: 51 unit tests pass on Node 24 and Node 22. This PR's CI build is the first nuxt build of these changes.
The order routes (get, put, add-product, remove-product, checkout, capture) call forwardToCms, which throws a CMS error on as createError({ statusCode, statusMessage, data: { message, errors?, missing? } }), built by the pure shopErrorFromCms (server/utils/cmsError.ts). Before, the FetchError was thrown on as it was: the browser got the CMS's status, but Nitro treated it as unhandled, answered "Server Error" without data and logged every CMS 4xx as [unhandled]. Now the browser also gets the CMS's message, the errors of a rejected update and the fields a checkout misses, and no other field. A status that is the shop's own fault (401, 403, ...) is answered 500, a CMS that does not answer 503; 5xx are logged without the query and the order uuid. npm test runs tests/unit with Node's type stripping and no dependencies, as in libreshop/cms. nuxt.config keeps tests/ out of the app's type check. Refs libretech/mp#71PUT /api/orders/:uuid forwarded the browser's body to the CMS unchanged. It now forwards { data } with only the seven fields of the checkout's steps: email, acceptedTermsAndConditionsAt, invoiceAddress, deliveryAddress, invoiceAddressStructured, deliveryAddressStructured and delivery. Any other field, a field beside data, or a body of another shape is answered 400 "Invalid order update" with the CMS's error format, without calling the CMS. The values are left to the CMS, which checks them and stays the authority; this is defence in depth. pickCustomerUpdate (server/utils/customerUpdate.ts) is pure and tested with the exact payloads of steps 1 and 2 and with every server-only attribute of the order. Refs libretech/mp#71checkoutErrorMessage (utils/checkoutError.ts) maps the status and body of a failed request, the shop's answer or a raw CMS (Strapi) error, to one fixed message for the customer, addressed with "du": an invalid e-mail address (when data.email is among the rejected fields), other invalid input, an order already paid, a product no longer available, an order not ready for checkout (naming the missing steps), a payment that does not fit the order or an order changed during the payment ("Bitte starte die Zahlung neu"), PayPal not reachable ("versuche es in ein paar Minuten noch einmal"), a payment PayPal may have taken that the CMS could not confirm or record ("Bitte bezahle nicht noch einmal"), and a general fallback. It never shows the server's text. Steps 1 and 2 show the message above their submit button, in the style of step 3's payment error, instead of logging the error only. Step 3 shows it for a failed PayPal order creation or capture. The e-mail input of step 1 is type="email" (Input.vue takes a type). Refs libretech/mp#71The CMS answers a capture of an order already paid with this PayPal order with { success: true, alreadyCaptured: true } instead of the order. Step 3 took that answer for a capture without authorisation and showed a payment error. It now reloads the paid order (keeping the shown one if the reload fails) and continues as after a first capture: confirmation, the checkout-payment-completed event, and the redirect to the order's result page. capturePayment is typed as Order | AlreadyCaptured, and the pure isAlreadyCaptured (utils/captureResponse.ts) tells them apart. Refs libretech/mp#71POST /api/contact posted { to, subject, body, replyTo } to ${mailApiUrl}/send, which the mail service (libreshop/mail src/app.py) does not have, so every message failed with a 500. It now posts { to_email, subject, message } to POST /v1/send/message, still to paperwork@muellerprints.de, with the same subject and text as before. The service sets no Reply-To; the sender's address stays in the text. A line break in the subject is replaced by a space, so it cannot start another mail header. mailApiUrl was read from runtimeConfig without being declared there, so NUXT_MAIL_API_URL could not set it and the fallback http://mail:2222 was always used. It is declared now, with that default; mp's compose.yml sets no NUXT_MAIL_API_URL. contactMail and mailSendUrl (server/utils/contactMail.ts) are pure and tested. Refs libretech/mp#71View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.