commit b0265357733b2cfcb245508297bbb5c317e478ec
parent 48b2b3454334bb6af3ca1c628208f187c058f6e2
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 31 Oct 2026 11:18:36 +0100
stick to proper spelling of build-in units
Diffstat:
3 files changed, 44 insertions(+), 37 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/merchant-webui-transactional-flows.ts b/packages/taler-harness/src/integrationtests/merchant-webui-transactional-flows.ts
@@ -237,7 +237,9 @@ export async function testLockedProductDeletion(
description: "Locked inventory regression fixture",
price: "CHF:1",
unit_price: ["CHF:1"],
- unit: "piece",
+ // Builtin unit slug: the backend resolves a product's unit against the
+ // unit tables and answers 404 MERCHANT_GENERIC_UNIT_UNKNOWN otherwise.
+ unit: "Piece",
total_stock: 10,
}),
);
diff --git a/packages/taler-merchant-webui/src/screens/CreateProductScreen.test.tsx b/packages/taler-merchant-webui/src/screens/CreateProductScreen.test.tsx
@@ -30,7 +30,7 @@ const product = {
soldCount: undefined,
category: "",
categories: [],
- unit: "piece",
+ unit: "Piece",
totalStock: -1,
};
diff --git a/packages/taler-merchant-webui/src/screens/CreateProductScreen.tsx b/packages/taler-merchant-webui/src/screens/CreateProductScreen.tsx
@@ -69,71 +69,76 @@ export interface CreateProductScreenProps {
* a component, and pogen extracts only strings written as t`` templates. As
* plain literals no unit name or description reached the catalogue, so the
* whole list stayed English in every language.
+ *
+ * Every slug has to be one of the backend's builtin unit identifiers: since
+ * protocol v25 the backend resolves a product's unit against its unit tables
+ * and answers 404 MERCHANT_GENERIC_UNIT_UNKNOWN for anything else. The
+ * `allowFraction` flags mirror `unit_allow_fraction` of those builtins.
*/
function predefinedUnits(
t: TranslateFn,
): { slug: string; name: string; allowFraction: boolean; desc: string }[] {
return [
{
- slug: "piece",
+ slug: "Piece",
name: t`Piece`,
allowFraction: false,
desc: t`Customers order whole pieces.`,
},
{
- slug: "bottle",
- name: t`Bottle`,
- allowFraction: false,
- desc: t`Customers order whole bottles.`,
- },
- {
- slug: "box",
- name: t`Box`,
- allowFraction: false,
- desc: t`Customers order whole boxes.`,
- },
- {
- slug: "portion",
- name: t`Portion`,
+ slug: "Set",
+ name: t`Set`,
allowFraction: false,
- desc: t`Customers order whole portions.`,
+ desc: t`Customers order whole sets.`,
},
{
- slug: "kilogram",
+ slug: "WeightUnitKg",
name: t`Kilogram (kg)`,
allowFraction: true,
desc: t`Customers can order fractions of a kilogram.`,
},
{
- slug: "gram",
+ slug: "WeightUnitG",
name: t`Gram (g)`,
allowFraction: true,
desc: t`Customers can order fractional grams.`,
},
{
- slug: "litre",
+ slug: "VolumeUnitLitre",
name: t`Litre (l)`,
allowFraction: true,
desc: t`Customers can order fractions of a litre.`,
},
{
- slug: "millilitre",
+ slug: "VolumeUnitCm3",
name: t`Millilitre (ml)`,
allowFraction: true,
desc: t`Customers can order fractional millilitres.`,
},
{
- slug: "metre",
+ slug: "SizeUnitM",
name: t`Metre (m)`,
allowFraction: true,
desc: t`Customers can order fractional metres.`,
},
{
- slug: "hour",
+ slug: "SizeUnitCm",
+ name: t`Centimetre (cm)`,
+ allowFraction: true,
+ desc: t`Customers can order fractional centimetres.`,
+ },
+ {
+ slug: "TimeUnitHour",
name: t`Hour (h)`,
allowFraction: true,
desc: t`Customers can order fractional hours.`,
},
+ {
+ slug: "TimeUnitDay",
+ name: t`Day (d)`,
+ allowFraction: true,
+ desc: t`Customers can order fractional days.`,
+ },
];
}
@@ -158,16 +163,16 @@ function formatAsAmountString(
}
const PREDEFINED_UNIT_SLUGS = new Set([
- "piece",
- "bottle",
- "box",
- "portion",
- "kilogram",
- "gram",
- "litre",
- "millilitre",
- "metre",
- "hour",
+ "Piece",
+ "Set",
+ "WeightUnitKg",
+ "WeightUnitG",
+ "VolumeUnitLitre",
+ "VolumeUnitCm3",
+ "SizeUnitM",
+ "SizeUnitCm",
+ "TimeUnitHour",
+ "TimeUnitDay",
]);
function dateInputValue(timestamp: TalerProtocolTimestamp | undefined): string {
@@ -207,7 +212,7 @@ export function CreateProductScreen({
const isEditing = !!editId;
const existing = initialProduct || productResource?.data;
- const existingUnit = existing?.unit || "piece";
+ const existingUnit = existing?.unit || "Piece";
// Form State (Spec 07 ยง 7.2)
const [name, setName] = useState<string>(existing?.name || "");
@@ -274,7 +279,7 @@ export function CreateProductScreen({
setDescription(existing.description || "");
setImage(existing.image || "");
setPrice(existing.price || (currency ? `${currency}:2.50` : ""));
- const loadedUnit = existing.unit || "piece";
+ const loadedUnit = existing.unit || "Piece";
setUnit(PREDEFINED_UNIT_SLUGS.has(loadedUnit) ? loadedUnit : "other");
setCustomUnit(PREDEFINED_UNIT_SLUGS.has(loadedUnit) ? "" : loadedUnit);
@@ -385,7 +390,7 @@ export function CreateProductScreen({
setErrorMsg("");
setIsSubmitting(true);
- const finalUnit = unit === "other" ? customUnit || "piece" : unit;
+ const finalUnit = unit === "other" ? customUnit || "Piece" : unit;
const finalProductId = productId || `prod_${slugify(name)}`;
const formattedPrice: AmountString = formatAsAmountString(price, currency);