From c7a422621ab7fcca3ec4d53be14aeeef6fd8cf91 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Fri, 9 Oct 2026 01:12:21 +0200 Subject: [PATCH] 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 https://git.librete.ch/libretech/mp/issues/71 --- src/checkout/order-state.ts | 17 +++++++++++++++++ tests/unit/order-state.test.ts | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/checkout/order-state.ts b/src/checkout/order-state.ts index 7c66128..4388349 100644 --- a/src/checkout/order-state.ts +++ b/src/checkout/order-state.ts @@ -8,3 +8,20 @@ export type OrderLockState = { * so the customer can no longer change its contact details, addresses, delivery method or cart. */ export const isLockedForCustomer = (order: OrderLockState): boolean => Boolean(order.paymentAuthorised); + +/** The fields of an order a checkout needs: the contact and address steps of the shop's checkout, and a delivery method. */ +export type CheckoutFields = { + email?: string | null; + invoiceAddress?: string | null; + /** The populated delivery method, or null. */ + delivery?: unknown; +}; + +/** The names of the fields the order still lacks for a checkout, in this order: email, invoiceAddress, delivery. [] if none. */ +export const missingCheckoutFields = (order: CheckoutFields): string[] => { + const missing: string[] = []; + if (typeof order.email !== "string" || order.email.trim() === "") missing.push("email"); + if (typeof order.invoiceAddress !== "string" || order.invoiceAddress.trim() === "") missing.push("invoiceAddress"); + if (order.delivery === null || order.delivery === undefined) missing.push("delivery"); + return missing; +}; diff --git a/tests/unit/order-state.test.ts b/tests/unit/order-state.test.ts index 3274d24..c07261a 100644 --- a/tests/unit/order-state.test.ts +++ b/tests/unit/order-state.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { isLockedForCustomer } from "../../src/checkout/order-state.ts"; +import { isLockedForCustomer, missingCheckoutFields } from "../../src/checkout/order-state.ts"; // Shaped like the row PUT /orders/:uuid/cart reads with strapi.db.query("api::order.order").findOne before it updates. const order = (overrides = {}) => ({ @@ -21,6 +21,36 @@ test("an order is open for the customer until its payment is authorised", () => }); test("an order is locked once its payment is authorised", () => { - // what capturePayment writes once PayPal reports the capture COMPLETED + // what capturePayment writes once the capture is verified (src/checkout/capture-payment.ts) + assert.equal(isLockedForCustomer(order({ paymentAuthorised: true, paymentStatus: "captured" })), true); + // what it wrote before assert.equal(isLockedForCustomer(order({ paymentAuthorised: true, paymentStatus: "authorised" })), true); }); + +// Shaped like the row POST /orders/:uuid/checkout reads (strapi.db.query findOne with orderDefaultParams), after both checkout steps. +const readyForCheckout = (overrides = {}) => + order({ + invoiceAddress: "Erika Mustermann\nMusterstraße 1\n70190 Stuttgart", + deliveryAddress: "Erika Mustermann\nMusterstraße 1\n70190 Stuttgart", + delivery: { id: 1, name: "DHL", price: 4.95 }, + ...overrides + }); + +test("an order that went through both checkout steps lacks nothing for the checkout", () => { + assert.deepEqual(missingCheckoutFields(readyForCheckout()), []); +}); + +test("names the email, invoice address and delivery method an order still lacks for the checkout", () => { + // a new order: POST /orders creates it empty + assert.deepEqual(missingCheckoutFields({ email: null, invoiceAddress: null, delivery: null }), ["email", "invoiceAddress", "delivery"]); + assert.deepEqual(missingCheckoutFields({}), ["email", "invoiceAddress", "delivery"]); + // after step 1 alone + assert.deepEqual(missingCheckoutFields(readyForCheckout({ invoiceAddress: null, deliveryAddress: null, delivery: null })), [ + "invoiceAddress", + "delivery" + ]); + assert.deepEqual(missingCheckoutFields(readyForCheckout({ email: " " })), ["email"]); + assert.deepEqual(missingCheckoutFields(readyForCheckout({ invoiceAddress: "" })), ["invoiceAddress"]); + // a delivery method that was deleted is populated as null + assert.deepEqual(missingCheckoutFields(readyForCheckout({ delivery: null })), ["delivery"]); +});