taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit aff5e1302efb6d7f74a525901728e9bb95a65f28
parent 435b4599b59bff8f0f4c51e57434b4291e149a53
Author: Florian Dold <dold@taler.net>
Date:   Tue,  3 Nov 2026 22:53:20 +0100

merchant web UI: prevent enabling stock tracking for untracked products

Diffstat:
Mpackages/taler-merchant-webui/src/screens/CreateProductScreen.test.tsx | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-merchant-webui/src/screens/CreateProductScreen.tsx | 24++++++++++++++----------
2 files changed, 90 insertions(+), 10 deletions(-)

diff --git a/packages/taler-merchant-webui/src/screens/CreateProductScreen.test.tsx b/packages/taler-merchant-webui/src/screens/CreateProductScreen.test.tsx @@ -49,6 +49,82 @@ async function submit(container: HTMLElement): Promise<void> { }); } +for (const mode of ["untracked", "tracked", "create"] as const) { + test(`CreateProductScreen stock tracking: ${mode}`, async () => { + const container = document.body.appendChild(document.createElement("div")); + const stocks: (number | undefined)[] = []; + try { + await act(async () => { + render( + <CreateProductScreen + editId={mode === "create" ? undefined : product.id} + initialProduct={ + mode === "create" + ? undefined + : { + ...product, + stockTracked: mode === "tracked", + totalStock: mode === "tracked" ? 10 : -1, + } + } + currency="EUR" + categories={[]} + onUpdateProduct={async (_id, patch) => { + stocks.push(patch.total_stock); + }} + onCreateProduct={async (_id, detail) => { + stocks.push(detail.total_stock); + }} + />, + container, + ); + }); + const checkbox = Array.from( + container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'), + ).find((input) => + input.closest("label")?.textContent?.includes("Count inventory stock"), + )!; + assert.equal(checkbox.disabled, mode === "untracked"); + assert.equal(checkbox.checked, mode === "tracked"); + if (mode === "untracked") { + act(() => checkbox.click()); + assert.equal(checkbox.checked, false); + assert.equal(container.querySelector("#prod_stock_count_input"), null); + assert.match( + container.textContent!, + /Stock tracking cannot be enabled/, + ); + } else { + if (mode === "create") { + setInput( + container.querySelector<HTMLInputElement>("#prod_name_input")!, + "New product", + ); + setInput( + container.querySelector<HTMLInputElement>("#prod_price_input")!, + "2.50", + ); + act(() => checkbox.click()); + } else { + // A draft toggle must not lock an originally tracked product. + act(() => checkbox.click()); + assert.equal(checkbox.disabled, false); + act(() => checkbox.click()); + } + setInput( + container.querySelector<HTMLInputElement>("#prod_stock_count_input")!, + "20", + ); + } + await submit(container); + assert.deepEqual(stocks, [mode === "untracked" ? -1 : 20]); + } finally { + act(() => render(null, container)); + container.remove(); + } + }); +} + for (const scenario of [ { name: "unchanged price", inputs: [], expected: "EUR:2.50" }, { name: "updated price", inputs: ["3.75"], expected: "EUR:3.75" }, diff --git a/packages/taler-merchant-webui/src/screens/CreateProductScreen.tsx b/packages/taler-merchant-webui/src/screens/CreateProductScreen.tsx @@ -300,15 +300,16 @@ function CreateProductForm({ ); // Stock Control (Spec 07 ยง 7.2) - const [countStock, setCountStock] = useState<boolean>( - Boolean( - existing?.stockTracked && - existing.totalStock !== undefined && - existing.totalStock >= 0, - ), + const existingStockTracked = Boolean( + existing?.stockTracked && + existing.totalStock !== undefined && + existing.totalStock >= 0, ); - const [totalStock, setTotalStock] = useState<string>( - countStock ? String(existing!.totalStock) : "", + // The backend cannot enable stock tracking for an existing untracked product. + const stockTrackingDisabled = isEditing && !existingStockTracked; + const [countStock, setCountStock] = useState<boolean>(existingStockTracked); + const [totalStock, setTotalStock] = useState<string>(() => + existingStockTracked ? String(existing!.totalStock) : "", ); const [nextRestock, setNextRestock] = useState<string>( dateInputValue(existing?.nextRestock), @@ -631,15 +632,18 @@ function CreateProductForm({ <input type="checkbox" checked={countStock} + disabled={stockTrackingDisabled} onChange={(e) => setCountStock((e.target as HTMLInputElement).checked) } - class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 cursor-pointer" + class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50" /> <span>{t`Count inventory stock for this product`}</span> </label> <p class="text-xs text-gray-500 mt-1 pl-6"> - {t`Enable to track quantity in stock and reserve items during checkout.`} + {stockTrackingDisabled + ? t`Stock tracking cannot be enabled for an existing product with unlimited inventory.` + : t`Enable to track quantity in stock and reserve items during checkout.`} </p> </div>