Wie der Throttler seit 886b4e7: die Sperre nach abgelehnten Schlüsseln
zählt über rateLimitSubject, damit wechselnde Adressen im eigenen
IPv6-/64 das Budget nicht zurücksetzen. Im Log steht weiter die Adresse.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
359 lines
19 KiB
Markdown
359 lines
19 KiB
Markdown
# ERP-Inventar-Export (ApiCore → ERPNext)
|
||
|
||
Stand: 2026-10-12 · Vertragsversion `1` · Betrifft: ApiCore-Backend (`src/erp-export/`), ERPNext-Abgleich (Teilprojekt T8)
|
||
|
||
Nur lesende Schnittstelle, über die ERPNext nachts den Inventar-Spiegel holt (ERPNext-Spec `2026-10-12-erpnext-ausbau-design.md`, Abschnitt 8.3, Entscheidungen E2 und E3). ApiCore bleibt führend für Bestand, Chargen und MHD; ERPNext spiegelt nur und bucht daraus Vertrauensschrank und Eigenbedarf.
|
||
|
||
Der ERPNext-Importer wird genau nach diesem Dokument gebaut. Änderungen am Vertrag gehen deshalb nur additiv (siehe [Versionierung](#10-versionierung)).
|
||
|
||
## 1. Zugang
|
||
|
||
| Punkt | Festlegung |
|
||
|---|---|
|
||
| Verfahren | API-Schlüssel als Bearer-Token: `Authorization: Bearer ack_<8 Zeichen>_<43 Zeichen>` |
|
||
| Recht | `erp-inventory:read` („Inventar-Spiegel lesen (ERPNext)“). Öffnet ausschließlich die vier Endpunkte unten. |
|
||
| Schreiben | Nicht möglich. Alle Endpunkte sind `GET`; das Recht öffnet keinen anderen Endpunkt (auch nicht `/shop-sync`). |
|
||
| Speicherung | ApiCore speichert nur den SHA-256-Hash. Gesucht wird über den Hash (eindeutiger Index), nie durch Vergleich des Geheimnisses. Der Schlüssel selbst wird genau einmal beim Anlegen angezeigt. |
|
||
| Abschalten | Jederzeit widerrufbar (wirkt sofort), optional mit Ablaufdatum. |
|
||
| Absender | Optional nur von bestimmten IP-Adressen/Netzen (Allowlist am Schlüssel). |
|
||
| Fehlversuche | Nach 20 abgelehnten Schlüsseln (fehlerhaft, unbekannt, widerrufen, abgelaufen) innerhalb von 15 Minuten antwortet ApiCore dieser IP-Adresse (bei IPv6 dem ganzen /64-Netz) mit `429`, auch für richtige Schlüssel. Zusätzlich gilt das allgemeine Limit von 100 Anfragen je Minute und IP. |
|
||
| Protokoll | Abgelehnte Schlüssel werden mit Grund, Präfix (`ack_abcd1234`) und IP protokolliert, nie mit dem Schlüssel. Query-Strings landen nicht im Log. |
|
||
| Trennung | Eigener Schlüssel nur für ERPNext. Der Wabenhain-Schlüssel (`shop-sync:*`) kommt an diese Endpunkte nicht heran und umgekehrt. |
|
||
|
||
Benutzer mit dem Recht `erp-inventory:read` (z. B. die Rolle Administrator) können die Endpunkte mit ihrem Login ebenfalls lesen, etwa zum Prüfen.
|
||
|
||
### Schlüssel anlegen (Inhaber)
|
||
|
||
1. ApiCore → Verwaltung → Automatisierung → API-Schlüssel.
|
||
2. Name: `ERPNext`. Unter „Was darf der Schlüssel?“ **nur** „Inventar-Spiegel lesen (ERPNext)“ ankreuzen (den vorausgewählten Shop-Haken entfernen).
|
||
3. IP-Adressen: die Adresse, von der ERPNext ApiCore erreicht (siehe [Rollout](#11-rollout)). Unbekannt? Leer lassen, einmal abrufen, dann am Schlüssel „Adressen ändern“ und die zuletzt benutzte Adresse übernehmen.
|
||
4. Schlüssel kopieren und **nur** als Kubernetes-Secret für den ERPNext-Abgleich hinterlegen (nicht in `config.json`, nicht ins Repo, nicht in Tickets).
|
||
|
||
## 2. Allgemeines
|
||
|
||
- Basis: `https://<apicore-host>/api/v1/export/inventar`
|
||
- Antworten: JSON (UTF-8), `Cache-Control: no-store`.
|
||
- Zeitpunkte: ISO 8601 in UTC mit Millisekunden, z. B. `2026-10-01T08:15:30.123Z`. Kalendertage (Abfülldatum, MHD, Erntedatum) stehen als Mitternacht UTC, z. B. `2026-08-01T00:00:00.000Z`; der Tag ist der Datumsteil.
|
||
- Kennungen (`id`, `productId`, `batchId`) sind undurchsichtige Zeichenketten (cuid), stabil über die Lebensdauer des Datensatzes.
|
||
- Mengen in **Stück** (Gläser bzw. hergestellte Stück) sind ganze Zahlen; Gewichte stehen mit Einheit im Feldnamen (`fillWeightGrams`, `netWeightKg`).
|
||
- Unbekannte Query-Parameter werden mit `400` abgelehnt.
|
||
|
||
### Seitenweises Lesen
|
||
|
||
`produkte`, `chargen` und `bewegungen` liefern Seiten in einer gemeinsamen Hülle:
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `contractVersion` | number | Vertragsversion, derzeit `1` |
|
||
| `generatedAt` | string (Zeitpunkt) | Zeitpunkt der Antwort |
|
||
| `items` | array | Einträge dieser Seite, stabil sortiert |
|
||
| `nextCursor` | string \| null | Position hinter dem letzten Eintrag. Bei leerer Seite der übergebene `nach`-Wert (oder `null`, wenn keiner übergeben wurde). |
|
||
| `hasMore` | boolean | `true`, wenn hinter dieser Seite weitere Einträge liegen |
|
||
|
||
| Parameter | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `limit` | Ganzzahl 1–500, Vorgabe 100 | Einträge je Seite |
|
||
| `nach` | Cursor | `nextCursor` der vorigen Antwort, unverändert. Undurchsichtig (base64url); nur von dem Endpunkt gültig, der ihn ausgegeben hat. |
|
||
|
||
Ablauf: ohne `nach` beginnen, dann mit `nach=<nextCursor>` weiterlesen, solange `hasMore` `true` ist.
|
||
|
||
`produkte` und `chargen` sind Momentaufnahmen und werden in jedem Lauf vollständig gelesen. Ändert sich zwischen zwei Seiten ein Bestand, gleicht der nächste Lauf das aus. Bei `bewegungen` speichert der Importer den letzten `nextCursor` und liest im nächsten Lauf dort weiter.
|
||
|
||
## 3. `GET /produkte` – Artikel
|
||
|
||
Alle ApiCore-Artikel (auch eingestellte), sortiert nach `id` aufsteigend.
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `id` | string | ApiCore-Produkt-ID |
|
||
| `name` | string | Bezeichnung |
|
||
| `sku` | string \| null | Artikelnummer in ApiCore (eindeutig, optional) |
|
||
| `kind` | `BOTTLED_HONEY` \| `MANUFACTURED` | Wo der Bestand liegt: Honig je Abfüllung, andere Waren (Kerzen, Propolis …) je Herstellungscharge |
|
||
| `shopCategory` | `HONIG` \| `KERZEN` \| `PROPOLIS` \| `SONSTIGES` | Kategorie/Sorte des Artikels |
|
||
| `status` | `ACTIVE` \| `DISCONTINUED` | eingestellt = nicht mehr hergestellt; Restbestände können noch existieren |
|
||
| `packagingSize` | number \| null | Gebindegröße, Einheit in ApiCore nicht festgelegt; Konvention **Gramm** (wie der Wabenhain-Import annimmt) |
|
||
| `weight` | number \| null | Gewicht, gleiche Einheiten-Konvention |
|
||
| `salesUnit` | string \| null | Verkaufseinheit, z. B. `Glas` |
|
||
| `updatedAt` | string (Zeitpunkt) | letzte Änderung am Artikel |
|
||
|
||
Für Honig ist die verlässliche Füllmenge `fillWeightGrams` an der Charge (siehe unten), nicht `packagingSize`.
|
||
|
||
**Verknüpfung zu den Shop-Artikeln `WH-<slug>`** (Wabenhain kennt die ApiCore-IDs):
|
||
|
||
- Honig: Wabenhain führt ein Shop-Produkt je Abfüllung; `Product.apicoreBottlingId` = `chargen[].id` mit `batchType = BOTTLING`. Der ApiCore-Artikel dazu steht in `chargen[].productId`.
|
||
- Andere Waren: `Product.apicoreProductId` = `produkte[].id` (Artikel mit `kind = MANUFACTURED`).
|
||
|
||
## 4. `GET /chargen` – Chargen mit Rest
|
||
|
||
Erst alle Abfüllungen (`BOTTLING`), dann alle Herstellungschargen (`PRODUCTION`), jeweils nach `id` aufsteigend. Rohstoff-Chargen (Wachs, Propolis) sind keine Ware und fehlen bewusst.
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `id` | string | ApiCore-ID der Charge |
|
||
| `batchType` | `BOTTLING` \| `PRODUCTION` | Abfüllung (Honig) oder Herstellungscharge |
|
||
| `lotNumber` | string | Losnummer (MHD-Etikett). Eindeutig **je Art**; über beide Arten hinweg nicht garantiert. Eindeutiger Schlüssel ist (`batchType`, `id`). |
|
||
| `productId` | string \| null | ApiCore-Artikel. Bei Abfüllungen optional (`null` = keinem Artikel zugeordnet). |
|
||
| `date` | string (Kalendertag) | Abfüll- bzw. Herstelldatum |
|
||
| `bestBeforeDate` | string (Kalendertag) \| null | MHD; bei Abfüllungen immer gesetzt, bei Herstellungschargen optional |
|
||
| `initialQuantity` | Ganzzahl (Stück) | abgefüllte Gläser bzw. hergestellte Stück |
|
||
| `remainingQuantity` | Ganzzahl (Stück) | Restbestand = `initialQuantity` + Summe aller Bewegungen. Kann negativ sein (Shop hat mehr verkauft als gebucht; in ApiCore rot „Prüfen“). |
|
||
| `fillWeightGrams` | number \| null | Füllmenge je Glas in Gramm; nur `BOTTLING` |
|
||
| `honeyType` | string \| null | Sorte der Honigcharge hinter der Abfüllung (z. B. `Sommertracht`); nur `BOTTLING`, `null` wenn nicht erfasst |
|
||
|
||
Hinweise für den Abgleich:
|
||
|
||
- `remainingQuantity` ist der Sollwert für den Bestandsabgleich (Stock Reconciliation) im Lager „Imkerei (ApiCore)“. Er enthält alle Bewegungen bis zum Abruf, auch die der letzten Minuten, die `/bewegungen` noch zurückhält.
|
||
- Wird `initialQuantity` in ApiCore korrigiert, ändert sich der Rest sofort mit.
|
||
- Eine irrtümlich angelegte Herstellungscharge ohne Bewegungen kann in ApiCore gelöscht werden; sie fehlt dann in der Liste und ist im Spiegel auf 0 zu setzen. Abfüllungen und Chargen mit Bewegungen werden nie gelöscht.
|
||
|
||
## 5. `GET /bewegungen` – Bestandsbewegungen
|
||
|
||
Jede Bestandsänderung einer Abfüllung oder Herstellungscharge, nie gelöscht (Fehler werden in ApiCore gegengebucht). Sortiert nach (`createdAt`, `id`) aufsteigend.
|
||
|
||
| Parameter | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `seit` | ISO-Zeitpunkt oder -Datum | Nur Bewegungen ab diesem Zeitpunkt (einschließlich). Für den ersten Lauf, z. B. `seit=2026-01-01T00:00:00Z`. Nicht zusammen mit `nach`. |
|
||
| `nach` | Cursor | Weiterlesen hinter der zuletzt gelieferten Bewegung |
|
||
| `limit` | 1–500 | wie oben |
|
||
|
||
Ohne `seit` und `nach` beginnt die Liste bei der ältesten Bewegung.
|
||
|
||
**Wartezeit:** Es erscheinen nur Bewegungen, die älter als **5 Minuten** sind. Eine Buchung bekommt ihren Zeitstempel beim Einfügen, sichtbar wird sie erst beim Abschluss ihrer Transaktion; ohne Wartezeit könnte der Cursor über eine noch offene, frühere Buchung hinweglaufen. Die Antwort nennt die Grenze zusätzlich:
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `settledUntil` | string (Zeitpunkt) | Bis hierhin ist die Liste vollständig (Abrufzeit minus 5 Minuten) |
|
||
|
||
Felder je Bewegung:
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `id` | string | ApiCore-ID der Bewegung; zum Entdoppeln im Importer |
|
||
| `createdAt` | string (Zeitpunkt) | Buchungszeitpunkt |
|
||
| `batchType` | `BOTTLING` \| `PRODUCTION` | Art der Charge |
|
||
| `batchId` | string | `chargen[].id` |
|
||
| `lotNumber` | string | Losnummer der Charge (zur Bequemlichkeit) |
|
||
| `productId` | string \| null | Artikel der Charge |
|
||
| `type` | siehe Tabelle | Art der Bewegung |
|
||
| `reason` | siehe Tabelle \| null | Grund, nur bei `WITHDRAWAL` |
|
||
| `quantity` | Ganzzahl (Stück) | mit Vorzeichen: **minus = aus dem Bestand**, plus = zurück in den Bestand |
|
||
|
||
| `type` | `reason` | Vorzeichen | Bedeutung | Behandlung in ERPNext (Spec 8.3) |
|
||
|---|---|---|---|---|
|
||
| `WITHDRAWAL` | `HOFLADEN` | − | Ware in den **Vertrauensschrank** gestellt | Umlagerung „Imkerei (ApiCore)“ → „Vertrauensschrank“ |
|
||
| `WITHDRAWAL` | `EIGENBEDARF` | − | Eigenverbrauch | unentgeltliche Wertabgabe zu Herstellkosten |
|
||
| `WITHDRAWAL` | `BRUCH` | − | Bruch | nur im Spiegel |
|
||
| `WITHDRAWAL` | `PROBE` | − | Probe/Muster | nur im Spiegel |
|
||
| `WITHDRAWAL` | `SONSTIGES` | − | sonstige Entnahme | nur im Spiegel |
|
||
| `SHOP_SALE` | `null` | − | **Verkauf** im Wabenhain-Shop (bezahlt) | nur im Spiegel; die Rechnung kommt aus dem Shop-Import |
|
||
| `SHOP_RETURN` | `null` | + | **Retoure**/Storno einer bezahlten Shop-Bestellung | nur im Spiegel |
|
||
| `CORRECTION` | `null` | ± | Inventurkorrektur (gezählt minus gebucht), auch zum Gegenbuchen einer Fehlbuchung | nur im Spiegel |
|
||
|
||
Rohstoff-Bewegungen (Wachs, Propolis, Art `CONSUMPTION`) gehören nicht zur Ware und erscheinen nie. Kommt künftig ein weiterer `type` oder `reason` hinzu, behandelt der Importer ihn als „nur im Spiegel“ und meldet ihn im Cockpit.
|
||
|
||
Ablauf im Importer: erster Lauf mit `seit=<Startdatum>`, danach immer mit dem gespeicherten `nextCursor`. Derselbe Cursor liefert bei erneutem Abruf dieselben Bewegungen (plus inzwischen hinzugekommene); der Importer entdoppelt trotzdem über `id`.
|
||
|
||
## 6. `GET /ernten` – Erntemengen je Jahr
|
||
|
||
Grundlage für die Honigkosten je kg (Imkerei-Ausgaben des Jahres / geerntete kg). Nicht seitenweise (eine Zeile je Jahr).
|
||
|
||
| Parameter | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `jahr` | Ganzzahl 1900–2100, optional | nur dieses Kalenderjahr; ohne Angabe alle Jahre mit Ernten |
|
||
|
||
| Feld | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `contractVersion` | number | `1` |
|
||
| `generatedAt` | string (Zeitpunkt) | Zeitpunkt der Antwort |
|
||
| `years[].year` | Ganzzahl | Kalenderjahr (aus dem Erntedatum, UTC) |
|
||
| `years[].netWeightKg` | number | Summe der Nettogewichte in **kg**, auf Gramm (3 Nachkommastellen) gerundet |
|
||
| `years[].harvestCount` | Ganzzahl | Anzahl Ernten im Jahr |
|
||
| `years[].harvestsWithoutNetWeight` | Ganzzahl | davon ohne erfasstes Nettogewicht (zählen nicht zur Menge) |
|
||
|
||
Definition wie die ApiCore-Statistik „Honigertrag je Jahr“: Summe `netWeight` der Ernten mit Erntedatum im Jahr. Mit `jahr` kommt immer genau eine Zeile, bei einem Jahr ohne Ernten mit Nullen. Ist `harvestsWithoutNetWeight > 0`, sind die Kosten je kg zu hoch – der Importer sollte das im Cockpit anzeigen. Standorte, Völker und Notizen sind nicht enthalten.
|
||
|
||
## 7. Fehler
|
||
|
||
Fehlerantworten sind JSON mit `statusCode` und `message` (bei Validierungsfehlern `message` als Liste).
|
||
|
||
| Status | Wann | Verhalten des Importers |
|
||
|---|---|---|
|
||
| `400` | `limit` außerhalb 1–500 oder keine Ganzzahl; `nach` kein gültiger Cursor oder von einem anderen Endpunkt; `seit` kein ISO-Zeitpunkt; `seit` und `nach` zusammen; `jahr` außerhalb 1900–2100; unbekannter Parameter | Programmfehler – Schritt abbrechen, im Cockpit melden |
|
||
| `401` | Schlüssel fehlt, ist fehlerhaft, unbekannt, widerrufen oder abgelaufen | Schritt abbrechen; Hinweis „ApiCore-Schlüssel prüfen“ |
|
||
| `403` | Schlüssel ohne `erp-inventory:read`, oder IP-Adresse nicht in der Allowlist des Schlüssels | Schritt abbrechen; Hinweis „Recht bzw. Adressen am Schlüssel prüfen“ |
|
||
| `429` | zu viele Fehlversuche oder Anfragen von dieser Adresse | Schritt abbrechen, im nächsten Lauf erneut |
|
||
| `5xx` / keine Antwort | Störung in ApiCore | Schritt abbrechen, im nächsten Lauf erneut |
|
||
|
||
Ein fehlgeschlagener Abruf ist nie „leer“: der Importer setzt dann keine Bestände auf 0 und schiebt keinen Cursor weiter. Die anderen Schritte des Abgleich-Jobs laufen weiter.
|
||
|
||
## 8. Beispiele
|
||
|
||
Alle Werte synthetisch.
|
||
|
||
```http
|
||
GET /api/v1/export/inventar/produkte?limit=2
|
||
Authorization: Bearer <ERPNEXT_APICORE_KEY>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"contractVersion": 1,
|
||
"generatedAt": "2026-10-13T02:00:04.512Z",
|
||
"items": [
|
||
{
|
||
"id": "cmg0honig500sommer01",
|
||
"name": "Sommertracht 500 g",
|
||
"sku": "HON-SO-500",
|
||
"kind": "BOTTLED_HONEY",
|
||
"shopCategory": "HONIG",
|
||
"status": "ACTIVE",
|
||
"packagingSize": 500,
|
||
"weight": 500,
|
||
"salesUnit": "Glas",
|
||
"updatedAt": "2026-09-02T07:41:10.000Z"
|
||
},
|
||
{
|
||
"id": "cmg0kerze0bienen0001",
|
||
"name": "Bienenwachskerze klein",
|
||
"sku": null,
|
||
"kind": "MANUFACTURED",
|
||
"shopCategory": "KERZEN",
|
||
"status": "ACTIVE",
|
||
"packagingSize": null,
|
||
"weight": 120,
|
||
"salesUnit": "Stück",
|
||
"updatedAt": "2026-09-20T15:03:55.000Z"
|
||
}
|
||
],
|
||
"nextCursor": "eyJrIjoicHJvZHVrdGUiLCJpZCI6ImNtZzBrZXJ6ZTBiaWVuZW4wMDAxIn0",
|
||
"hasMore": true
|
||
}
|
||
```
|
||
|
||
```http
|
||
GET /api/v1/export/inventar/chargen?limit=500
|
||
```
|
||
|
||
```json
|
||
{
|
||
"contractVersion": 1,
|
||
"generatedAt": "2026-10-13T02:00:05.130Z",
|
||
"items": [
|
||
{
|
||
"id": "cmg0abf0sommer2026a1",
|
||
"batchType": "BOTTLING",
|
||
"lotNumber": "L-2026-014",
|
||
"productId": "cmg0honig500sommer01",
|
||
"date": "2026-08-01T00:00:00.000Z",
|
||
"bestBeforeDate": "2028-08-01T00:00:00.000Z",
|
||
"initialQuantity": 60,
|
||
"remainingQuantity": 49,
|
||
"fillWeightGrams": 500,
|
||
"honeyType": "Sommertracht"
|
||
},
|
||
{
|
||
"id": "cmg0her0kerze2026b01",
|
||
"batchType": "PRODUCTION",
|
||
"lotNumber": "K-2026-003",
|
||
"productId": "cmg0kerze0bienen0001",
|
||
"date": "2026-09-01T00:00:00.000Z",
|
||
"bestBeforeDate": null,
|
||
"initialQuantity": 20,
|
||
"remainingQuantity": 19,
|
||
"fillWeightGrams": null,
|
||
"honeyType": null
|
||
}
|
||
],
|
||
"nextCursor": "eyJrIjoiY2hhcmdlbiIsInQiOiJQIiwiaWQiOiJjbWcwaGVyMGtlcnplMjAyNmIwMSJ9",
|
||
"hasMore": false
|
||
}
|
||
```
|
||
|
||
```http
|
||
GET /api/v1/export/inventar/bewegungen?seit=2026-01-01T00:00:00Z&limit=3
|
||
```
|
||
|
||
```json
|
||
{
|
||
"contractVersion": 1,
|
||
"generatedAt": "2026-10-13T02:00:05.877Z",
|
||
"settledUntil": "2026-10-13T01:55:05.877Z",
|
||
"items": [
|
||
{
|
||
"id": "cmg0bew000000000001",
|
||
"createdAt": "2026-09-05T16:20:11.402Z",
|
||
"batchType": "BOTTLING",
|
||
"batchId": "cmg0abf0sommer2026a1",
|
||
"lotNumber": "L-2026-014",
|
||
"productId": "cmg0honig500sommer01",
|
||
"type": "WITHDRAWAL",
|
||
"reason": "HOFLADEN",
|
||
"quantity": -6
|
||
},
|
||
{
|
||
"id": "cmg0bew000000000002",
|
||
"createdAt": "2026-09-06T09:02:47.118Z",
|
||
"batchType": "BOTTLING",
|
||
"batchId": "cmg0abf0sommer2026a1",
|
||
"lotNumber": "L-2026-014",
|
||
"productId": "cmg0honig500sommer01",
|
||
"type": "SHOP_SALE",
|
||
"reason": null,
|
||
"quantity": -3
|
||
},
|
||
{
|
||
"id": "cmg0bew000000000003",
|
||
"createdAt": "2026-09-12T18:44:03.950Z",
|
||
"batchType": "BOTTLING",
|
||
"batchId": "cmg0abf0sommer2026a1",
|
||
"lotNumber": "L-2026-014",
|
||
"productId": "cmg0honig500sommer01",
|
||
"type": "WITHDRAWAL",
|
||
"reason": "EIGENBEDARF",
|
||
"quantity": -2
|
||
}
|
||
],
|
||
"nextCursor": "eyJrIjoiYmV3ZWd1bmdlbiIsInRzIjoiMjAyNi0wOS0xMlQxODo0NDowMy45NTBaIiwiaWQiOiJjbWcwYmV3MDAwMDAwMDAwMDAzIn0",
|
||
"hasMore": true
|
||
}
|
||
```
|
||
|
||
```http
|
||
GET /api/v1/export/inventar/ernten?jahr=2026
|
||
```
|
||
|
||
```json
|
||
{
|
||
"contractVersion": 1,
|
||
"generatedAt": "2026-10-13T02:00:06.240Z",
|
||
"years": [
|
||
{
|
||
"year": 2026,
|
||
"netWeightKg": 184.35,
|
||
"harvestCount": 7,
|
||
"harvestsWithoutNetWeight": 1
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
```json
|
||
{ "statusCode": 400, "message": "Ungültiger Cursor - bitte nextCursor der vorigen Seite unverändert als \"nach\" übergeben", "error": "Bad Request" }
|
||
```
|
||
|
||
## 9. Was nicht enthalten ist
|
||
|
||
Bewusst nicht im Export (Feld-Allowlist im Code, `src/erp-export/erp-export.service.ts`): Notizen an Chargen und Bewegungen, Benutzer (wer gebucht hat), API-Schlüssel und Shop-Anfrage-Kennungen, Artikelbeschreibungen (in ApiCore interne Notizen), Standorte, Völker, Behandlungen, Rohstoffe. Neue Felder an den ApiCore-Modellen erscheinen erst, wenn sie hier ausdrücklich ergänzt werden.
|
||
|
||
## 10. Versionierung
|
||
|
||
`contractVersion` steigt nur bei inkompatiblen Änderungen (Feld entfernt, umbenannt oder in der Bedeutung geändert). Neue Felder, neue Enum-Werte und neue Endpunkte kommen ohne Versionswechsel; der Importer ignoriert unbekannte Felder und bricht bei einer anderen `contractVersion` ab, ohne etwas zu ändern.
|
||
|
||
## 11. Rollout
|
||
|
||
1. ApiCore ausrollen. Keine Datenbank-Migration nötig: das Recht `erp-inventory:read` legt ApiCore beim Start an und gibt es der Systemrolle Administrator.
|
||
2. Schlüssel anlegen (Abschnitt 1).
|
||
3. Schlüssel als Kubernetes-Secret für den ERPNext-Abgleich ablegen, dazu die Basis-URL (`…/api/v1`) als Konfiguration.
|
||
4. Erreichbarkeit: wie der Wabenhain-Shop über die öffentliche ApiCore-Adresse. Dann ist die Absender-IP die Ausgangsadresse des Clusters, und die Allowlist des Schlüssels kann genau diese Adresse enthalten. Ein Aufruf über den clusterinternen Service ginge mit wechselnder Pod-Adresse ein und bräuchte eine NetworkPolicy-Freigabe; die Allowlist taugt dann nur noch als Netzbereich.
|
||
5. Probeabruf aller vier Endpunkte, dann den nächtlichen Schritt aktivieren.
|