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:
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>