Commit Graph
5 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5.5 3bc0693e4d refactor(apicore-sync): API key and shop feed only, no password login
The sync now talks to ApiCore exclusively through the secured shop
interface: GET /shop-sync with an API key as bearer token. Removed:

- the email/password service-account login (and its MFA handling),
- the fallback to ApiCore's general read endpoints (/harvests,
  /products, /extractions, /bottling-batches, /apiaries).

A missing interface (404) or a rejected key (401/403) now stops the run
with a specific message instead of falling back. APICORE_SYNC_EMAIL and
APICORE_SYNC_PASSWORD are gone from .env.example, README and the k8s
secret template; the system status needs APICORE_API_KEY.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 06:05:11 +02:00
Nick SzaunigandClaude Opus 5.5 d38f4303ff feat(apicore-sync): field-level ownership between ApiCore and the shop
ApiCore stays the source of truth for beekeeping facts, Wabenhain for
everything sales-related; both remain separate packages talking only over
ApiCore's HTTP API.

- Batches: lot number, fill date, best-before date and filled jar count
  now follow ApiCore on every run (before: created once, corrections never
  arrived). remainingGlasses - the shop's own stock - is never written; if
  it exceeds a corrected fill quantity, the run reports it instead of
  silently adjusting.
- Batches are matched by the new Batch.apicoreBottlingId instead of the lot
  number, so correcting a lot number in ApiCore no longer creates a second
  batch. Existing synced batches are adopted once via their lot number.
- Harvests: date and year always follow ApiCore, also when published or
  edited; type, location and text stay as maintained in the admin.
- Products stay entirely shop-owned after creation (weight drives the
  legally required unit price, so it is never changed automatically).
- Admin panel: batch update count and stock-conflict notice; intro text
  states which system owns which fields.

Migration is additive (nullable unique column).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:14:35 +02:00
Nick SzaunigandClaude Opus 5.5 0482fd490e fix(apicore-sync): keep internal notes out of the shop and protect edited drafts
- Harvest descriptions no longer include ApiCore's internal harvest notes.
  The description becomes public on "Veröffentlichen", so a draft published
  without a careful read could have carried operational notes into the shop.
- A repeated sync overwrote unpublished harvest drafts, undoing the admin's
  corrections (season instead of honey type, location, sales text). Drafts
  with an admin audit entry are now left alone and reported as protected.
- Every ApiCore request has a 20 s timeout; before, an ApiCore that accepted
  the connection but never answered left the admin action hanging.
- The sync panel shows the time of the last complete run.
- Client tests: timeout, failed fetch is never treated as an empty source,
  malformed responses are rejected as a whole.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 05:39:09 +02:00
Nick SzaunigandClaude Opus 5 c525735285 feat: ein Shop-Produkt je Jahrgang statt je ApiCore-Artikel
Der Produkt-Sync bildete den Fall gleichnamiger Jahrgaenge nicht ab. In
ApiCore ist "Waldhonig 500 g" EIN Katalogartikel, an dem jedes Jahr eine
weitere Abfuellung haengt; der Sync legte daraus genau ein Wabenhain-Produkt
an. Ab dem zweiten Jahr passierte damit nichts mehr: das Produkt existierte
schon und wurde uebersprungen, und die Charge des neuen Jahrgangs blieb
unverknuepft, weil linkProductsToBatches nur eingesprungen ist, solange
Product.batchId leer war.

In Wabenhain gehoert ein Produkt zu genau einer Charge, und das ist richtig
so: jeder Jahrgang hat eigenen Preis, eigenen Verkaufstext, eigenes
Geschmacksprofil und eigene Bewertungen. Honig ist ein Naturprodukt, die
Bewertung des Waldhonigs von 2026 sagt ueber den von 2027 nichts aus.

Der Sync laeuft deshalb jetzt ueber die ABFUELLUNGEN statt ueber den
Artikelkatalog und legt je Abfuellung einen Jahrgang an, bereits mit Charge
und Ernte verknuepft. Die nachtraegliche Verknuepfung entfaellt ersatzlos.

Wiedererkannt wird ein Jahrgang ueber die neue Spalte
Product.apicoreBottlingId (nullable, unique; manuell angelegte Produkte
tragen null). Bewusst nicht ueber den Slug: der ist die oeffentliche Adresse
und soll lesbar bleiben. Er traegt stattdessen den Jahrgang
("waldhonig-2026") und faellt bei mehrfacher Abfuellung im selben Jahr auf
die Losnummer zurueck - hochgezaehlt wird nicht, "waldhonig-2026-2" sagt
niemandem etwas.

Weitere Entscheidungen, jeweils zugunsten dessen, was tatsaechlich im Glas
ist: das Gewicht kommt aus der Glasgroesse der Abfuellung statt aus dem
Artikel, und der Lagerbestand startet bei 0 statt bei der Abfuellmenge -
wie viele Glaeser in den Webshop gehen und wie viele in Hofladen oder
Wiederverkauf, entscheidet Wabenhain.

Ein in ApiCore ausgelaufener Artikel bekommt keinen neuen Jahrgang mehr,
bereits importierte bleiben unveraendert im Verkauf: solange Glaeser im
Regal stehen, ist "nicht mehr produziert" kein Grund, den Abverkauf
abzubrechen. Fehlt die Charge eines Jahrgangs noch, meldet der Lauf das als
"wartet auf den Ernten-Sync" statt als Fehler.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 03:24:02 +02:00
Nick SzaunigandClaude Opus 5 78c19f1b73 feat: Produkte aus ApiCore synchronisieren
Der Shop konnte Ernten und Chargen bereits aus ApiCore uebernehmen, den
Produktkatalog aber nicht - jedes Glas musste im Admin ein zweites Mal von
Hand angelegt werden. Neuer Knopf unter /admin/produkte zieht die Produkte
und traegt anschliessend die passende Charge am Produkt nach, wo ApiCore
die Abfuellung einem Produkt zugeordnet hat. Genau diese Verknuepfung
fuettert die Herkunftsseite hinter dem QR-Code auf dem Glas.

ApiCore fuehrt ueberhaupt keine Verkaufspreise. Importierte Produkte
entstehen deshalb als deaktivierte Entwuerfe mit Preis 0, den nur ein
Mensch setzen kann. Damit daraus nie eine Gratis-Bestellung wird, lehnen
die Aktivierung im Admin und der Checkout ein Produkt ohne Preis jetzt
ausdruecklich ab, und die Produktliste kennzeichnet solche Entwuerfe.

Ein bereits vorhandenes Produkt wird uebersprungen, nie ueberschrieben:
genau die drei Felder, die ApiCore liefert (Name, Beschreibung, Gewicht),
zieht der Admin vor der Freischaltung von Hand nach - ein Update wuerde
diese Arbeit bei jedem weiteren Lauf wegwerfen. Ein in ApiCore
ausgelaufenes Produkt wird nicht automatisch deaktiviert, sondern nur
gemeldet, damit der Restbestand abverkauft werden kann.

Nebenbei behoben: die beiden Produktformulare und die Bestelluebersicht
haben den ?error=-Parameter aus fail() nie ausgegeben - abgelehnte
Eingaben landeten wortlos wieder auf dem Formular.

Der Login gegen ApiCore ist jetzt von den Abfragen getrennt
(openApiCoreSession), damit ein Sync-Lauf mit einer einzigen Anmeldung
auskommt statt mit dreien. Die Feld-Abbildung liegt neu in
lib/apicore-mapping.ts und ist damit testbar, statt hinter "use server"
zu verschwinden.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-16 18:03:05 +02:00