From b59be8e2013a5a88efc1b5d39a64bbd9a54af50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Rosenl=C3=B6cher?= Date: Mon, 19 Oct 2026 19:39:12 +0200 Subject: [PATCH] fix(labels): eindeutige Platzhalter-Namen, echte Beispiele, Hinweis bei doppeltem Inhalt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Die Werte waren richtig verdrahtet; irreführend waren die Namen: „Chargennummer“ (Honigcharge) und „Los-/Chargennummer der Abfüllung“ lasen sich gleich. Jetzt mit Herkunft im Namen, z. B. „Chargennummer der Honigcharge (nicht Los)“, „Los-Zeile („L“ + Los-Nr.)“, „MHD (nur Datum)“, und einem Beispiel in Druckform unter „Inhalt“. - Beispielwerte entsprechen dem Druck und sind alle verschieden. - Etiketten-Prüfung: neue Warnung DUPLICATE_TEXT, wenn zwei Felder den gleichen Text drucken oder Nummer und Zeile nebeneinander stehen (nicht blockierend, abwärtskompatibel). Co-Authored-By: Claude Opus 5.5 (1M context) --- ARCHITECTURE.md | 2 +- .../label-templates/label-particulars.spec.ts | 79 +++++++++++++++++++ .../src/label-templates/label-particulars.ts | 57 ++++++++++++- .../element-properties-panel.spec.tsx | 15 ++++ .../designer/element-properties-panel.tsx | 12 ++- .../designer/label-designer.spec.tsx | 4 +- .../src/lib/label-templates/check.ts | 3 +- .../label-templates/placeholder-keys.spec.ts | 39 ++++++++- .../lib/label-templates/placeholder-keys.ts | 63 ++++++++++----- 9 files changed, 245 insertions(+), 29 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 45b1cdf..040e539 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -828,7 +828,7 @@ Für `MEDICINE_LOT`, `AUTHORITY_NOTICE` und `APIARY_RESTRICTION` gilt zusätzlic ### 12.4 Etikett-Pflichtangaben -`checkLabel` (`src/label-templates/label-particulars.ts`) ist rein und ohne Datenbank; Regeln und Grenzen: Annahmen 65 bis 68. **Pflichtangaben** (sperren den Druck: 409 `LABEL_PARTICULARS_MISSING` mit `{ gaps }`, Abschnitt 12.6): `LEGAL_NAME` (ein Wort mit „honig“), `NET_QUANTITY` (Füllmenge der Abfüllung, „250,5 g“ wie „250.5 g“, „1 kg“ nur bei vollen Kilogramm; als ganze Wörter), `BEST_BEFORE` („Mindestens haltbar bis“ mit dem Datum), `LOT`, `ORIGIN` und `FOOD_BUSINESS` (Name und Anschrift). **Warnungen** (sperren nie): `LOT_PREFIX`, `X_HEIGHT` (unter `LabelTemplate.minXHeightMm`), `NET_QUANTITY_HEIGHT` (Ziffernhöhe unter der Regel aus `ComplianceSettings.netQuantityDigitRules`), `TEXT_CLIPPED`. Das Ergebnis trägt `requirements` (`minXHeightMm`, `netQuantityMinCapHeightMm`) und je Befund `elementIds` (bei Lücken die abgeschnittenen Elemente, die die Angabe getragen hätten). **Verfeinerungen (Runden 2 bis 4 der Task 5).** +`checkLabel` (`src/label-templates/label-particulars.ts`) ist rein und ohne Datenbank; Regeln und Grenzen: Annahmen 65 bis 68. **Pflichtangaben** (sperren den Druck: 409 `LABEL_PARTICULARS_MISSING` mit `{ gaps }`, Abschnitt 12.6): `LEGAL_NAME` (ein Wort mit „honig“), `NET_QUANTITY` (Füllmenge der Abfüllung, „250,5 g“ wie „250.5 g“, „1 kg“ nur bei vollen Kilogramm; als ganze Wörter), `BEST_BEFORE` („Mindestens haltbar bis“ mit dem Datum), `LOT`, `ORIGIN` und `FOOD_BUSINESS` (Name und Anschrift). **Warnungen** (sperren nie): `LOT_PREFIX`, `X_HEIGHT` (unter `LabelTemplate.minXHeightMm`), `NET_QUANTITY_HEIGHT` (Ziffernhöhe unter der Regel aus `ComplianceSettings.netQuantityDigitRules`), `TEXT_CLIPPED`, `DUPLICATE_TEXT` (zwei Felder drucken denselben Inhalt, sobald ein Platzhalter beteiligt ist, oder `lotNumber` neben `lotLine` bzw. `bestBeforeDate` neben `bestBeforeLine`; gleicher fester Text zählt nicht). Das Ergebnis trägt `requirements` (`minXHeightMm`, `netQuantityMinCapHeightMm`) und je Befund `elementIds` (bei Lücken die abgeschnittenen Elemente, die die Angabe getragen hätten). **Verfeinerungen (Runden 2 bis 4 der Task 5).** - **Ziffern-Wächter je Element.** „Zahl direkt vor der Menge“ („1,250 g“ ist kein Treffer für 250 g) gilt nur im Text eines Elements, nie im verbundenen Text: Eine Losnummer oder ein Datum in einem Nachbarelement macht „500 g“ nicht ungültig. „250g“ gilt als „250 g“, „L2026“ bleibt. - **Lesereihenfolge** kommt allein aus den Positionen (nach y, Zeilen mit 1 mm Toleranz gegen die erste Oberkante, je Zeile von links nach rechts), nie aus der Reihenfolge der Elemente in der Vorlage. - **Verteilte Angaben** (Name und Anschrift): Zeilen, Blöcke in Lesereihenfolge oder Ketten wortgleicher Elemente, jeweils mit **symmetrischem** senkrechtem Abstand von höchstens 10 mm (`max(0, y − Unterkante, Oberkante − (y + h))`, gemessen an der laufenden Ober- und Unterkante der ganzen Gruppe, nicht am Vorgänger, `groupByDistance`). Elemente mit leerem normalisiertem Text werden ignoriert und überbrücken nichts; eine hohe Textbox überbrückt Zeilen um ihre Boxhöhe. Nur sichtbare (nicht abgeschnittene) Teile zählen (Annahme 75). diff --git a/ApiCore-Backend/src/label-templates/label-particulars.spec.ts b/ApiCore-Backend/src/label-templates/label-particulars.spec.ts index 6571553..f71243b 100644 --- a/ApiCore-Backend/src/label-templates/label-particulars.spec.ts +++ b/ApiCore-Backend/src/label-templates/label-particulars.spec.ts @@ -919,3 +919,82 @@ describe('SAMPLE_BOTTLING', () => { expect(values.lotLine).toBe('L 2026-0001'); }); }); + +describe('checkLabel: the same content twice', () => { + function duplicates(elements: LabelTextElement[], values = input().values) { + return checkLabel(input({ elements, values })).warnings.filter( + (warning) => warning.code === 'DUPLICATE_TEXT', + ); + } + + it('a full label without repeats has no DUPLICATE_TEXT', () => { + expect(duplicates(fullLabel())).toEqual([]); + }); + + it('warns when one placeholder is placed twice', () => { + const [finding] = duplicates([ + ...fullLabel(), + textElement('legal-2', 'legalName', 86), + ]); + expect(finding.elementIds).toEqual(['legal', 'legal-2']); + expect(finding.detail).toContain('„Blütenhonig“'); + }); + + it('warns when two placeholders print the same value (honey type = article name)', () => { + const values = { + ...input().values, + honeyType: 'Blütenhonig', + productName: 'Blütenhonig', + }; + const [finding] = duplicates( + [ + ...fullLabel(), + textElement('type', 'honeyType', 86), + textElement('name', 'productName', 98), + ], + values, + ); + expect(finding.elementIds).toEqual(['legal', 'type', 'name']); + }); + + it('warns when the lot number and the lot line both print the lot', () => { + const [finding] = duplicates([ + ...fullLabel(), + textElement('lot-number', 'lotNumber', 86), + ]); + expect(finding.elementIds).toEqual(['lot-number', 'lot']); + expect(finding.detail).toContain('„2026-0001“'); + }); + + it('warns when the best-before date and the best-before line both print the date', () => { + const [finding] = duplicates([ + ...fullLabel(), + textElement('bb-date', 'bestBeforeDate', 86), + ]); + expect(finding.elementIds).toEqual(['bb-date', 'bb']); + }); + + it('does not warn about repeated fixed text (the designer shows it as is)', () => { + expect( + duplicates([ + ...fullLabel(), + textElement('brand-1', null, 86, { text: 'Wabenhain' }), + textElement('brand-2', null, 98, { text: 'Wabenhain' }), + ]), + ).toEqual([]); + }); + + it('does not warn about placeholders without a value', () => { + const values = { ...input().values, honeyType: '' }; + expect( + duplicates( + [ + ...fullLabel(), + textElement('type-1', 'honeyType', 86), + textElement('type-2', 'honeyType', 98), + ], + values, + ), + ).toEqual([]); + }); +}); diff --git a/ApiCore-Backend/src/label-templates/label-particulars.ts b/ApiCore-Backend/src/label-templates/label-particulars.ts index fbbc50c..dd987fe 100644 --- a/ApiCore-Backend/src/label-templates/label-particulars.ts +++ b/ApiCore-Backend/src/label-templates/label-particulars.ts @@ -23,7 +23,8 @@ export type LabelFindingCode = | 'LOT_PREFIX' | 'X_HEIGHT' | 'NET_QUANTITY_HEIGHT' - | 'TEXT_CLIPPED'; + | 'TEXT_CLIPPED' + | 'DUPLICATE_TEXT'; export interface LabelFinding { code: LabelFindingCode; @@ -96,6 +97,7 @@ const LEGAL_NAME_WORD = 'honig'; interface Candidate { element: LabelTextElement; + text: string; normalized: string; clipped: boolean; } @@ -452,7 +454,7 @@ function candidatesOf( normalized !== '' && measurer.heightOfStringPt(text, element) > element.height * MM_TO_PT + CLIP_TOLERANCE_PT; - return { element, normalized, clipped }; + return { element, text, normalized, clipped }; }, ); } @@ -476,6 +478,52 @@ function unique(ids: string[]): string[] { return [...new Set(ids)]; } +// The first key prints a value the second key's line already holds. +const PRINTED_WITHIN: [PlaceholderKey, PlaceholderKey][] = [ + ['lotNumber', 'lotLine'], + ['bestBeforeDate', 'bestBeforeLine'], +]; + +/** + * Elements that print the same content twice: equal text where a placeholder + * is involved (fixed text the designer shows as is), or a value next to the + * line that already holds it ("2026-0001" and "L 2026-0001"). + */ +function duplicateTextFinding(candidates: Candidate[]): LabelFinding | null { + const filled = candidates.filter((candidate) => candidate.normalized !== ''); + const byText = new Map(); + for (const candidate of filled) { + byText.set(candidate.normalized, [ + ...(byText.get(candidate.normalized) ?? []), + candidate, + ]); + } + const withKey = (key: PlaceholderKey) => + filled.filter((candidate) => candidate.element.placeholderKey === key); + const groups = [ + ...[...byText.values()].filter( + (group) => + group.length > 1 && + group.some((candidate) => candidate.element.placeholderKey), + ), + ...PRINTED_WITHIN.map(([value, line]) => [withKey(value), withKey(line)]) + .filter(([values, lines]) => values.length > 0 && lines.length > 0) + .map(([values, lines]) => [...values, ...lines]), + ]; + if (groups.length === 0) { + return null; + } + const texts = unique( + groups.map((group) => `„${group[0].text.replace(/\s+/g, ' ').trim()}“`), + ); + return { + code: 'DUPLICATE_TEXT', + title: 'Gleicher Inhalt mehrfach', + detail: `Mehrere Felder drucken denselben Inhalt: ${texts.join(', ')}. Prüfen, ob das gewollt ist, sonst ein Feld entfernen.`, + elementIds: unique(groups.flat().map(({ element }) => element.id)), + }; +} + function xHeightFinding( carriers: Candidate[], minXHeightMm: number, @@ -635,6 +683,11 @@ export function checkLabel(input: LabelCheckInput): LabelCheckResult { warnings.push(netQuantityHeight); } + const duplicate = duplicateTextFinding(candidates); + if (duplicate) { + warnings.push(duplicate); + } + if (clippedCarriers.size > 0) { warnings.push({ code: 'TEXT_CLIPPED', diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.spec.tsx b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.spec.tsx index 43d0dfa..6f7aea0 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.spec.tsx @@ -60,4 +60,19 @@ describe("ElementPropertiesPanel", () => { await userEvent.click(screen.getByRole("button", { name: "Kursiv" })); expect(onChange).toHaveBeenCalledWith({ italic: true }); }); + + it("explains a placeholder under the content field, with the sample it prints", () => { + renderPanel({ ...TEXT, placeholderKey: "lotNumber" }); + + const content = screen.getByRole("combobox", { name: "Inhalt" }); + expect(content).toHaveAccessibleDescription( + "Nur die Nummer der Abfüllung, ohne „L“. Mit „L“ davor die Los-Zeile nehmen, nicht beides. Beispiel: „2026-0001“", + ); + }); + + it("shows no placeholder hint for fixed text", () => { + renderPanel(TEXT); + + expect(screen.getByRole("combobox", { name: "Inhalt" })).not.toHaveAccessibleDescription(); + }); }); diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx index 0d84294..edfad9e 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/element-properties-panel.tsx @@ -10,6 +10,7 @@ import { PLACEHOLDER_KEYS, PLACEHOLDER_LABELS, } from "@/lib/label-templates/types"; +import { PLACEHOLDER_HINTS, PLACEHOLDER_SAMPLES } from "@/lib/label-templates/placeholder-keys"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -135,18 +136,25 @@ function TextFields({ element: Extract; onChange: (patch: Partial) => void; }) { + const key = element.placeholderKey; return ( <>
onChange({ placeholderKey: key === "" ? null : key })} + value={key ?? ""} + onChange={(next) => onChange({ placeholderKey: next === "" ? null : next })} values={PLACEHOLDER_KEYS} labels={PLACEHOLDER_LABELS} noneLabel="Fester Text" + describedBy={key ? "element-text-source-hint" : undefined} /> + {key ? ( +

+ {PLACEHOLDER_HINTS[key]} Beispiel: „{PLACEHOLDER_SAMPLES[key]}“ +

+ ) : null}
{!element.placeholderKey ? (
diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.spec.tsx b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.spec.tsx index 6105ce9..a6d7c62 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.spec.tsx @@ -78,11 +78,11 @@ describe("LabelDesigner placeholder menu", () => { expect(insertMenu).not.toBeNull(); await userEvent.click(insertMenu as HTMLElement); - await userEvent.click(await screen.findByRole("option", { name: "Chargennummer" })); + await userEvent.click(await screen.findByRole("option", { name: "Chargennummer der Honigcharge (nicht Los)" })); // The placeholder element was added and is now edited in the properties panel. expect(await screen.findByRole("combobox", { name: "Inhalt" })).toHaveTextContent( - "Chargennummer", + "Chargennummer der Honigcharge (nicht Los)", ); expect(insertMenu).toHaveTextContent("Platzhalter einfügen"); expect(insertMenu).not.toHaveTextContent("batchNumber"); diff --git a/ApiCore-Frontend/src/lib/label-templates/check.ts b/ApiCore-Frontend/src/lib/label-templates/check.ts index e260ed5..3ebc493 100644 --- a/ApiCore-Frontend/src/lib/label-templates/check.ts +++ b/ApiCore-Frontend/src/lib/label-templates/check.ts @@ -23,7 +23,8 @@ export type LabelFindingCode = | "LOT_PREFIX" | "X_HEIGHT" | "NET_QUANTITY_HEIGHT" - | "TEXT_CLIPPED"; + | "TEXT_CLIPPED" + | "DUPLICATE_TEXT"; /** The six particulars that block printing, in the order the panel lists them. */ export const PARTICULAR_CODES = [ diff --git a/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.spec.ts b/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.spec.ts index a4b6751..c0f8573 100644 --- a/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.spec.ts +++ b/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.spec.ts @@ -1,7 +1,12 @@ import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { PLACEHOLDER_KEYS, PLACEHOLDER_LABELS } from "./placeholder-keys"; +import { + PLACEHOLDER_HINTS, + PLACEHOLDER_KEYS, + PLACEHOLDER_LABELS, + PLACEHOLDER_SAMPLES, +} from "./placeholder-keys"; // The monorepo sits next to the frontend; vitest runs from ApiCore-Frontend. // The frontend image build only sees ApiCore-Frontend, so the comparison runs @@ -26,4 +31,36 @@ describe("placeholder keys", () => { expect(PLACEHOLDER_LABELS[key]).toBeTruthy(); } }); + + it("names every key differently, so no two menu entries read alike", () => { + const labels = PLACEHOLDER_KEYS.map((key) => PLACEHOLDER_LABELS[key]); + expect(new Set(labels).size).toBe(labels.length); + // The honey batch number is not the lot of the bottling. + expect(PLACEHOLDER_LABELS.batchNumber).not.toBe(PLACEHOLDER_LABELS.lotNumber); + expect(PLACEHOLDER_LABELS.batchNumber).toMatch(/Honigcharge/); + expect(PLACEHOLDER_LABELS.lotNumber).toMatch(/Abfüllung/); + }); + + it("has a short hint for every key", () => { + for (const key of PLACEHOLDER_KEYS) { + expect(PLACEHOLDER_HINTS[key]).toBeTruthy(); + } + }); + + it("shows distinct samples, so the preview tells the placeholders apart", () => { + const samples = PLACEHOLDER_KEYS.map((key) => PLACEHOLDER_SAMPLES[key]); + expect(new Set(samples).size).toBe(samples.length); + }); + + // Same wording as buildPlaceholderValues in the backend. + it("shows samples in the form the label prints them", () => { + expect(PLACEHOLDER_SAMPLES.lotNumber).not.toMatch(/^L/); + expect(PLACEHOLDER_SAMPLES.lotLine).toBe(`L ${PLACEHOLDER_SAMPLES.lotNumber}`); + expect(PLACEHOLDER_SAMPLES.bestBeforeDate).toMatch(/^\d{2}\.\d{2}\.\d{4}$/); + expect(PLACEHOLDER_SAMPLES.bottlingDate).toMatch(/^\d{2}\.\d{2}\.\d{4}$/); + expect(PLACEHOLDER_SAMPLES.bestBeforeLine).toBe( + `Mindestens haltbar bis ${PLACEHOLDER_SAMPLES.bestBeforeDate}`, + ); + expect(PLACEHOLDER_SAMPLES.originCountry).toBe("Deutschland"); + }); }); diff --git a/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.ts b/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.ts index 62fc333..8e36e15 100644 --- a/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.ts +++ b/ApiCore-Frontend/src/lib/label-templates/placeholder-keys.ts @@ -20,38 +20,61 @@ export const PLACEHOLDER_KEYS = [ ] as const; export type PlaceholderKey = (typeof PLACEHOLDER_KEYS)[number]; +/** Menu and field names. Each one says where the value comes from. */ export const PLACEHOLDER_LABELS: Record = { - batchNumber: "Chargennummer", - honeyType: "Honigsorte", - lotNumber: "Los-/Chargennummer der Abfüllung", + batchNumber: "Chargennummer der Honigcharge (nicht Los)", + honeyType: "Honigsorte der Honigcharge", + lotNumber: "Los-/Chargennummer der Abfüllung (nur Nummer)", bottlingDate: "Abfülldatum", - bestBeforeDate: "Mindesthaltbarkeitsdatum (MHD)", - jarSize: "Glasgröße / Füllmenge", - productName: "Produktname", - legalName: "Verkehrsbezeichnung (z. B. Blütenhonig)", - originCountry: "Herkunftsland", + bestBeforeDate: "MHD (nur Datum)", + jarSize: "Füllmenge (Gramm)", + productName: "Artikelname", + legalName: "Verkehrsbezeichnung des Artikels (z. B. Blütenhonig)", + originCountry: "Herkunftsland (nur Land)", foodBusinessName: "Lebensmittelunternehmer: Name", foodBusinessAddress: "Lebensmittelunternehmer: Anschrift", - bestBeforeLine: "Zeile „Mindestens haltbar bis …“", - lotLine: "Zeile „L Losnummer“", + bestBeforeLine: "MHD-Zeile („Mindestens haltbar bis …“)", + lotLine: "Los-Zeile („L“ + Los-/Chargennummer)", +}; + +/** One sentence under the content field: what prints, and what not to combine. */ +export const PLACEHOLDER_HINTS: Record = { + batchNumber: + "Nummer der Honigcharge (Ernte). Sie ist nicht die Losnummer der Abfüllung und erfüllt die Pflichtangabe „Los“ nicht.", + honeyType: "Sorte an der Honigcharge. Kann gleich lauten wie Artikelname oder Verkehrsbezeichnung.", + lotNumber: + "Nur die Nummer der Abfüllung, ohne „L“. Mit „L“ davor die Los-Zeile nehmen, nicht beides.", + bottlingDate: "Tag der Abfüllung, nicht das MHD.", + bestBeforeDate: + "Nur das Datum. „Mindestens haltbar bis“ davor selbst setzen oder die MHD-Zeile nehmen, nicht beides.", + jarSize: "Füllmenge der Abfüllung.", + productName: "Name des Artikels im Sortiment.", + legalName: "Steht am Artikel. Ist sie dort leer, druckt das Feld nichts.", + originCountry: "Land aus den Einstellungen, ohne „Herkunft:“ davor.", + foodBusinessName: "Aus den Einstellungen.", + foodBusinessAddress: "Aus den Einstellungen, auch mehrzeilig.", + bestBeforeLine: "Ganze Zeile mit Datum. Nicht zusätzlich das MHD setzen.", + lotLine: "Ganze Zeile mit „L“ und Nummer der Abfüllung. Nicht zusätzlich die Los-/Chargennummer setzen.", }; /** * What the designer preview shows for a placeholder instead of the raw - * `{{key}}`: a plausible value, so sizes and line breaks look like the print. + * `{{key}}`: a plausible value in the form the label prints it (see + * `buildPlaceholderValues` in the backend), each one different so the preview + * tells the placeholders apart. */ export const PLACEHOLDER_SAMPLES: Record = { - batchNumber: "WH-2026-0001", - honeyType: "Blütenhonig", - lotNumber: "L-0001", - bottlingDate: "15.10.2026", - bestBeforeDate: "10/2028", + batchNumber: "HB-2026-001", + honeyType: "Sommerhonig", + lotNumber: "2026-0001", + bottlingDate: "01.07.2026", + bestBeforeDate: "01.07.2028", jarSize: "500 g", - productName: "Wabenhain Blütenhonig", + productName: "Wabenhain Sommerhonig", legalName: "Blütenhonig", - originCountry: "Herkunft: Deutschland", + originCountry: "Deutschland", foodBusinessName: "Max Mustermann", foodBusinessAddress: "Musterstr. 1\n86450 Altenmünster", - bestBeforeLine: "Mindestens haltbar bis Ende 10/2028", - lotLine: "L 0001", + bestBeforeLine: "Mindestens haltbar bis 01.07.2028", + lotLine: "L 2026-0001", };