import type { ApiCoreHarvestWithApiaryName, ApiCoreProduct } from "./apicore-client"; /** * Reine Feld-Abbildung ApiCore -> Wabenhain. Bewusst getrennt von * admin/actions/apicore-sync.ts: jene Datei traegt "use server" und darf * deshalb ausschliesslich async Server Actions exportieren — dort waeren * genau die Entscheidungen, die man pruefen moechte (Was wird aus einem * Produkt ohne Beschreibung? Was aus einem fehlenden Gewicht?), nicht * einzeln testbar. */ /** * Stabiler, eindeutiger Slug aus der ApiCore-ID — zugleich der * Idempotenz-Schluessel des Syncs: ein wiederholter Lauf erkennt daran, ob * ein ApiCore-Datensatz schon einmal importiert wurde, statt bei jedem Lauf * Duplikate anzulegen. Bewusst NICHT aus Name/Jahr abgeleitet (wie bei den * manuellen Anlege-Formularen): ein umbenannter Artikel bekaeme sonst einen * neuen Slug und wuerde ein zweites Mal importiert. */ export function slugForApiCoreSource(sourceId: string): string { return `apicore-${sourceId}`; } /** * Wabenhains Harvest.description ist ein Pflichtfeld, ApiCore liefert dafuer * kein Aequivalent — baut stattdessen eine sinnvolle Vorbefuellung aus den * vorhandenen Mengenfeldern, die der Admin beim Review verfeinert. * * ApiCores `notes` werden bewusst NICHT uebernommen: das sind interne * Betriebsnotizen des Imkers (Beobachtungen, Probleme, Behandlungshinweise), * und die Beschreibung wird mit dem Veroeffentlichen oeffentlich. Ein Klick * auf "Veroeffentlichen" ohne Durchlesen duerfte sie sonst in den Shop tragen. */ export function buildHarvestDescription(source: ApiCoreHarvestWithApiaryName): string { const parts: string[] = []; if (source.honeySuperCount != null) parts.push(`${source.honeySuperCount} Honigräume`); if (source.grossWeight != null) parts.push(`Bruttogewicht ${source.grossWeight} kg`); if (source.netWeight != null) parts.push(`Nettogewicht ${source.netWeight} kg`); const detail = parts.length > 0 ? parts.join(" · ") : "Bitte Beschreibung ergänzen."; return `Automatisch aus ApiCore importiert. ${detail}`; } /** * Wabenhains Product.description ist ein Pflichtfeld und steht im Shop als * Verkaufstext. ApiCores Beschreibung ist dagegen eine interne Notiz und oft * leer. * * Sie wird deshalb zwar uebernommen, aber immer mit einem Herkunftshinweis * versehen: der Text bleibt als Entwurf erkennbar, solange ihn niemand * ersetzt hat. Ist gar nichts da, tritt an seine Stelle eine ausdrueckliche * Aufforderung statt eines leeren Feldes — ein leerer Verkaufstext waere im * Shop nicht von einem vergessenen zu unterscheiden. */ export function buildProductDescription(source: ApiCoreProduct, vintageYear?: number): string { const own = source.description?.trim(); const facts: string[] = []; if (vintageYear != null) facts.push(`Jahrgang ${vintageYear}`); if (source.sku?.trim()) facts.push(`Artikelnummer ${source.sku.trim()}`); if (source.salesUnit?.trim()) facts.push(`Verkaufseinheit ${source.salesUnit.trim()}`); const detail = own || "Bitte Produktbeschreibung für den Shop ergänzen."; const suffix = facts.length > 0 ? ` (${facts.join(" · ")})` : ""; return `Automatisch aus ApiCore importiert${suffix}. ${detail}`; } /** * Gewicht in Gramm. Bevorzugt ApiCores `weight` (Gewicht des Artikels), * ersatzweise `packagingSize` (Gebindegroesse) — zur Einheiten-Annahme * "Gramm" siehe den Kommentar an productSchema in apicore-client.ts. * * Fehlt beides, wird bewusst 0 zurueckgegeben statt geraten. 0 ist in * Wabenhain kein gueltiges Gewicht (adminProductNumbersSchema verlangt * mindestens 1 g), das importierte Produkt laesst sich also gar nicht * abspeichern, ohne dass jemand ein echtes Gewicht eintraegt — genau die * gewuenschte Bremse, denn aus dem Gewicht ergibt sich der Grundpreis je * Kilogramm, der in Deutschland verpflichtend anzugeben ist. */ export function deriveWeightGrams(source: ApiCoreProduct): number { const raw = source.weight ?? source.packagingSize; if (raw == null || !Number.isFinite(raw) || raw <= 0) return 0; return Math.round(raw); } /** * Gewicht eines Glases aus dieser Abfuellung. * * Bevorzugt `jarSize` der Abfuellung gegenueber den Angaben am * ApiCore-Artikel: die Abfuellung sagt, was TATSAECHLICH in dieses Glas * gefuellt wurde, der Artikel nur, was ueblicherweise vorgesehen ist. Wird * ein Jahrgang einmal in 250-g- statt 500-g-Glaeser gefuellt, steht das an * der Abfuellung und nirgends sonst. * * Zur Einheit "Gramm" siehe den Kommentar an productSchema in * apicore-client.ts: ApiCore fuehrt jarSize mit Werten wie 500 und gibt sie * in der eigenen Oberflaeche als "g" aus. */ export function deriveJarWeightGrams( jarSize: number | null | undefined, source: ApiCoreProduct ): number { if (jarSize != null && Number.isFinite(jarSize) && jarSize > 0) return Math.round(jarSize); return deriveWeightGrams(source); } /** Aus "Waldhonig, cremig" wird "waldhonig-cremig". */ export function slugifyProductName(name: string): string { return name .toLowerCase() .replace(/ä/g, "ae") .replace(/ö/g, "oe") .replace(/ü/g, "ue") .replace(/ß/g, "ss") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } /** * Moegliche oeffentliche Adressen fuer ein synchronisiertes Produkt, in * absteigender Wuenschbarkeit — der Sync nimmt die erste noch freie. * * Der Jahrgang gehoert in die Adresse, weil genau er die Produkte * unterscheidet: "Waldhonig" heisst jedes Jahr gleich, ist aber jedes Jahr * ein anderes Erzeugnis mit eigenem Preis und eigenen Bewertungen. Ohne * Jahreszahl kollidierte der zweite Jahrgang mit dem ersten (Product.slug ist * eindeutig). * * Die Losnummer als zweite Stufe deckt den selteneren Fall ab, dass ein * Artikel im selben Jahr mehrfach abgefuellt wurde. Faellt auch die weg, * entscheidet der Aufrufer — hier wird bewusst nicht hochgezaehlt: eine * Adresse wie "waldhonig-2026-2" sagt niemandem etwas. */ export function productSlugCandidates(name: string, year: number, lotNumber: string): string[] { const base = slugifyProductName(name) || "produkt"; const withYear = `${base}-${year}`; const lot = slugifyProductName(lotNumber); return lot ? [withYear, `${withYear}-${lot}`] : [withYear]; }