> ## Documentation Index
> Fetch the complete documentation index at: https://help.polygon-one.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stapelimport (asynchron)

> Artikel, Lieferanten und Bestellungen im Stapel importieren — Job anlegen, Status abfragen, Ergebnis abholen

## Übersicht

Stapelimporte laufen **asynchron**: Der `POST` legt einen Import-Job an und antwortet
sofort mit `202` und einer `jobId`. Ihr System fragt anschließend den Job-Status ab
(`GET /api/import-jobs/{jobId}`), bis der Import abgeschlossen ist, und erhält dort das
vollständige Ergebnis — inklusive der Gründe für übersprungene oder fehlgeschlagene Zeilen.

Der Import nutzt dieselbe Engine wie der CSV-Import in der App: gleiche Validierung,
gleiches idempotentes Upsert, gleiches Ergebnisformat.

| Endpunkt                       | Vorgang                            | Upsert-Schlüssel               |
| :----------------------------- | :--------------------------------- | :----------------------------- |
| `POST /api/articles`           | Artikel im Stapel importieren      | `internalArticleNr`            |
| `POST /api/suppliers`          | Lieferanten im Stapel importieren  | `externalSupplierId` bzw. Name |
| `POST /api/orders`             | Bestellungen im Stapel importieren | `orderNumber`                  |
| `GET /api/import-jobs/{jobId}` | Job-Status und Ergebnis abfragen   | —                              |

<Info>
  Ein `POST` mit einem **einzelnen Objekt** (ohne `rows`-Array) bleibt synchron und
  antwortet unverändert mit `201`. Nur die Stapel-Form (Array bzw. Envelope) ist asynchron.
</Info>

## Ablauf

1. **Stapel senden** — `POST` mit einem Array von Zeilen oder einem Envelope-Objekt:

   ```json theme={null}
   { "rows": [ { "…": "…" } ] }
   ```

   Bei Erfolg: `202` mit `{ "jobId": "…", "status": "queued", "statusUrl": "/api/import-jobs/…" }`
   (auch als `Location`-Header). **Zu diesem Zeitpunkt ist noch nichts importiert.**
   Die Zeilen-Validierung läuft synchron — ein fehlerhafter Payload wird sofort mit `400`
   abgelehnt, ohne dass ein Job entsteht.

2. **Status abfragen** — `GET /api/import-jobs/{jobId}` etwa alle 2 Sekunden, bei großen
   Jobs mit steigendem Abstand (z. B. exponentiell bis \~30 s). `completed` und `failed`
   sind final — danach nicht weiter abfragen.

3. **Ergebnis auswerten** — bei `status: "completed"` enthält `result` das
   zusammengeführte Import-Ergebnis (`imported`, `updated`, `failed`, `skipped`, …
   je nach Entität). Fehlgeschlagene Zeilen tragen stabile, nicht lokalisierte
   Fehlerschlüssel.

## Limits

* **Maximal 5.000 Zeilen** pro Anfrage. Darüber: `413 { "error": "batchTooLarge", "maxBatchSize": 5000 }` —
  es wird nichts angelegt. Größere Abgleiche auf mehrere Anfragen aufteilen.
* **\~4,5 MB Request-Body** (Plattform-Limit, greift vor unserer Anwendung).
* Ein **leeres Array** oder ein fehlerhaftes Envelope → `400`.

## Teil-Akzeptanz und Idempotenz

* Gültige Zeilen werden übernommen, auch wenn andere Zeilen in `failed` landen.
* Ein identischer Import lässt sich gefahrlos wiederholen: bereits vorhandene Zeilen
  kommen als übersprungen/aktualisiert zurück, es entstehen keine Duplikate.

## Bestellungen: `replaceLines` und die Vollständigkeitsregel

Der Import einer Bestellung **ersetzt deren komplette Positionsliste**. Eine Anfrage,
die Bestellung `O` enthält, muss deshalb **alle** Positionen von `O` enthalten. Werden
die Positionen einer Bestellung über mehrere Anfragen verteilt, würde die zweite Anfrage
die zuvor importierten Positionen löschen.

Die **Positions-Schutzsperre** macht daraus einen lauten Fehler statt eines stillen Datenverlusts:

* Standard (`replaceLines` fehlt oder `false`): Würde der Stapel gespeicherte Positionen
  löschen, schlägt der **gesamte Job vor dem ersten Schreibvorgang** fehl. Im `error`-Feld
  des Jobs steht:

  ```json theme={null}
  { "key": "lineReplacementRequired", "orderNumbers": ["O"] }
  ```

  Senden Sie die betroffenen Bestellungen mit ihrer **vollständigen** Positionsliste erneut.
* `"replaceLines": true` autorisiert das Ersetzen: Die gesendete Positionsliste wird
  maßgeblich, auch wenn dabei Positionen entfallen.

## Fehlerschlüssel (stabil, nicht lokalisiert)

| Schlüssel                 | Bedeutung                                                                                                                                           |
| :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `batchTooLarge`           | Anfrage überschreitet das 5.000-Zeilen-Limit (HTTP `413`)                                                                                           |
| `lineReplacementRequired` | Ein Bestell-Job würde gespeicherte Positionen löschen — vollständige Positionslisten senden oder `replaceLines` setzen                              |
| `dispatchFailed`          | Job wurde angelegt, aber die Verarbeitung konnte nicht gestartet werden (HTTP `500`); es läuft keine Arbeit                                         |
| `invalidPayload`          | Die Zeilen sind bei der erneuten Prüfung im Worker durchgefallen; `error` enthält eine JSON-Zusammenfassung (`path` + `code`, nie die Werte selbst) |
| `workerError`             | Unerwarteter Fehler nach allen Wiederholungsversuchen (Details serverseitig erfasst)                                                                |
| `dbError`                 | Pro Zeile: unerwarteter Datenbankfehler ohne spezifischen Schlüssel                                                                                 |

## Aufbewahrung

Das Job-Ergebnis bleibt abfragbar; der gestagte Eingabe-Payload wird \~24 Stunden nach
Abschluss freigegeben, der Job-Datensatz \~30 Tage nach Erstellung gelöscht. Fragen Sie
das Ergebnis innerhalb dieses Fensters ab und persistieren Sie es auf Ihrer Seite.

## Berechtigungen

Der `POST` erfordert den **Write-Scope** der Entität (`articles:write`, `suppliers:write`,
`orders:write`), das Abfragen des Jobs den jeweiligen **Read-Scope** (im Write-Scope enthalten).
Jobs sind strikt mandantengebunden: eine unbekannte `jobId` und die `jobId` eines anderen
Unternehmens liefern dieselbe `404`.
