# Einheiten aus der Fertigung

> Seriennummern fertiger Batterien mit einem Schlüssel mit Scope import als Import bereitstellen; Mitglieder prüfen, übernehmen, geben frei und veröffentlichen im Portal.

Wenn die Fertigung fertige Batterien zurückmeldet, sollen ihre Seriennummern zu Einheiten in BatteryPass werden,
ohne dass jemand sie abtippt. In diesem Ablauf stellt die Middleware mit einem Schlüssel mit Scope `import` einen
Tabellenimport bereit. **Das Übernehmen des Imports und das Freigeben seiner Werte bleiben bei Mitgliedern Ihrer
Organisation im Portal**: Ein Schlüssel kann die Daten vorbereiten, deklarierte Daten gelangen aber nur durch die
Entscheidung einer Person in BatteryPass (und, wo Ihre Organisation es eingeschaltet hat, im Vier-Augen-Prinzip).

```mermaid
sequenceDiagram
  participant ERP
  participant MW as Middleware
  participant BP as BatteryPass-API
  participant M as Mitglied im Portal
  ERP->>ERP: Wareneingang buchen, Outbox-Zeile schreiben
  MW->>ERP: Outbox-Zeilen lesen (Seriennummer, GTIN, Datum)
  MW->>BP: createImport mit Datei und Preset (Scope import)
  BP-->>MW: 201 Import, Status uploaded
  MW->>BP: validateImport (Scope import)
  BP-->>MW: 202 Prüfauftrag
  loop alle paar Sekunden
    MW->>BP: getImport (Scope import)
  end
  BP-->>MW: Status validated, Zusammenfassung
  MW->>ERP: Zeilen als erledigt markieren, Zusammenfassung melden
  M->>BP: Prüfbericht lesen, Import übernehmen
  M->>BP: importierte Werte freigeben
  M->>BP: Pässe veröffentlichen
```

**Auslöser:** Die Fertigungsrückmeldung oder der Wareneingang aus der Fertigung bucht die Seriennummern fertiger Batterien.

**Scopes des Schlüssels:** import

| # | Wer                | Was                                                                                                                                                                                         | Operation                                                                                                        |
| - | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 1 | ERP                | Bucht Rückmeldung oder Wareneingang mit den Seriennummern und hält die Änderung in einer Ausgangstabelle (Outbox) fest.                                                                     |                                                                                                                  |
| 2 | Middleware         | Sammelt die Seriennummern einer Charge mit Modellkennung oder GTIN und Herstelldatum und schreibt eine CSV- oder XLSX-Datei mit den Spaltennamen der Vorlage (Preset).                      |                                                                                                                  |
| 3 | Middleware         | Lädt die Datei mit dem Preset hoch und merkt sich die Import-ID.                                                                                                                            | [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import.md) (Scope: import)     |
| 4 | Middleware         | Startet die Prüfung aller Zeilen.                                                                                                                                                           | [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import.md) (Scope: import) |
| 5 | Middleware         | Fragt den Import ab, bis er validated oder failed ist, und meldet die Zusammenfassung (gültig, Warnungen, Fehler, Duplikate) an die Fertigung zurück.                                       | [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import.md) (Scope: import)           |
| 6 | Mitglied im Portal | Prüft den Prüfbericht im Portal und übernimmt den Import; die Einheiten entstehen als Entwürfe.                                                                                             |                                                                                                                  |
| 7 | Mitglied im Portal | Gibt die importierten Werte in einem Schritt frei (ADR-015); ist das Vier-Augen-Prinzip der Organisation eingeschaltet, bleiben Werte offen, die die freigebende Person selbst erfasst hat. |                                                                                                                  |
| 8 | Mitglied im Portal | Veröffentlicht die Pässe, sobald keine Pflichtangabe mehr fehlt.                                                                                                                            |                                                                                                                  |

**Sicherheit**

- Ein eigener Schlüssel nur mit dem Scope import; er kann Importe bereitstellen, aber weder übernehmen noch freigeben.
- Ein wiederholter Upload legt einen zweiten Import an; seine Zeilen werden zu Duplikaten, sobald der erste übernommen ist, sodass keine Einheit doppelt entsteht.
- Die Outbox im ERP sorgt dafür, dass keine Buchung verloren geht, wenn die Middleware oder BatteryPass gerade nicht erreichbar ist.

**Datenminimierung**

- Die Datei enthält Seriennummer, Modellkennung oder GTIN, Herstelldatum und höchstens Produktionsstätte und Modellversion.
- Kunden, Aufträge, Preise und Namen von Beschäftigten bleiben im ERP.

## Vorbereitung

- Eine Inhaberin oder ein Inhaber legt unter **Einstellungen → API-Schlüssel** einen Schlüssel nur mit dem Scope
  `import` an, zum Beispiel `MES Linie 2 Importe`. Er kann weder Ihre Pässe lesen noch Zustandsdaten melden noch
  etwas übernehmen.
- Die Batteriemodelle gibt es in BatteryPass mit GTIN oder Modellkennung, und eine Modellversion deckt die
  Herstelldaten ab, die Sie senden. Der Import findet das Modell jeder Zeile über GTIN, Modellkennung oder
  Produktname.
- Wählen Sie eine [Importvorlage (Preset)](https://passport.kabin.sh/docs/de/integrations#erp-systeme), die zu dem passt, was Ihre
  Middleware schreibt. Die Vorlage `generic-csv` nimmt die Spalten `GTIN`, `Seriennummer`, `Herstelldatum`,
  `Produktionsstätte` (Name oder GLN), `Modellversion` und `Batteriestatus`, auf Deutsch oder Englisch.
  ERP-spezifische Vorlagen (SAP, Business Central, Odoo, NetSuite) stehen auf der jeweiligen
  [ERP-Seite](https://passport.kabin.sh/docs/de/integrations/erp/sap-s4hana-cloud).

## 1. Datei schreiben

Die Middleware sammelt die Seriennummern einer Charge aus ihrer Outbox und schreibt eine CSV-Datei (UTF-8,
getrennt durch `;`, `,`, Tabulator oder `|`) oder eine XLSX-Datei mit den Spaltennamen der Vorlage. Höchstens
10 MB, 10 000 Zeilen und 200 Spalten je Datei. Spalten, die die Vorlage nicht kennt, bleiben ohne Zuordnung;
zusätzliche Spalten schaden also nicht.

```text title="charge-2026-10-24.csv"
GTIN;Seriennummer;Herstelldatum;Produktionsstätte
04012345000023;SV2027-000901;2026-10-23;Werk Landshut
04012345000023;SV2027-000902;2026-10-23;Werk Landshut
```

## 2. Mit der Vorlage hochladen

[`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import) nimmt die Datei als `multipart/form-data` im Feld
`file` und die Vorlage im Feld `preset`. Die Antwort ist der Import mit der Zuordnung aus der Vorlage:

```bash title="cURL"
curl -sS -X POST "$BATTERYPASS_URL/api/v1/imports" \
  -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" \
  -F "file=@charge-2026-10-24.csv;type=text/csv" \
  -F "preset=generic-csv"
```

```ts title="TypeScript"
import { readFile } from 'node:fs/promises';

const BASE = `${process.env.BATTERYPASS_URL}/api/v1`;
const auth = { Authorization: `Bearer ${process.env.BATTERYPASS_IMPORT_KEY}` };

const csv = await readFile('charge-2026-10-24.csv', 'utf8');
const form = new FormData();
form.append('file', new Blob([csv], { type: 'text/csv' }), 'charge-2026-10-24.csv');
form.append('preset', 'generic-csv');
const res = await fetch(`${BASE}/imports`, { method: 'POST', headers: auth, body: form });
const imp = await res.json();
if (res.status !== 201) throw new Error(`createImport: ${res.status} ${imp.code} (request_id ${imp.request_id})`);
console.log(imp.id, imp.status, imp.preset);
```

```python title="Python"
import os

import requests

BASE = os.environ["BATTERYPASS_URL"] + "/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_IMPORT_KEY"]

with open("charge-2026-10-24.csv", "rb") as f:
    res = session.post(f"{BASE}/imports", files={"file": ("charge-2026-10-24.csv", f, "text/csv")},
                       data={"preset": "generic-csv"}, timeout=60)
imp = res.json()
if res.status_code != 201:
    raise SystemExit(f"createImport: {res.status_code} {imp['code']} (request_id {imp.get('request_id')})")
print(imp["id"], imp["status"], imp["preset"])
```

```json title="201 (gekürzt)"
{
  "id": "0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "status": "uploaded",
  "filename": "charge-2026-10-24.csv",
  "format": "csv",
  "row_count": 2,
  "headers": ["GTIN", "Seriennummer", "Herstelldatum", "Produktionsstätte"],
  "mapping": [
    { "column": 0, "header": "GTIN", "target": "identification.gtin" },
    { "column": 1, "header": "Seriennummer", "target": "identification.serial_number" },
    { "column": 2, "header": "Herstelldatum", "target": "identification.manufacturing_date" },
    { "column": 3, "header": "Produktionsstätte", "target": "identification.manufacturing_place" }
  ],
  "options": {},
  "preset": "generic-csv",
  "created_by_api_key": { "id": "0192f0aa-1b2c-7d3e-8f40-5a6b7c8d9e01", "name": "MES Linie 2 Importe" },
  "created_at": "2026-10-24T06:05:11Z",
  "updated_at": "2026-10-24T06:05:11Z",
  "row_version": 1
}
```

Der Import nennt den Schlüssel in `created_by_api_key`; Mitglieder sehen ihn in der Importliste des Portals als von
diesem Schlüssel bereitgestellt.

## 3. Optional: Optionen setzen

Hat die Datei keine Modell- oder keine Produktionsstätten-Spalte, setzen Sie den Wert für alle Zeilen mit
[`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping). Die Operation ersetzt die Zuordnung;
senden Sie also die Vorlage erneut (oder ein vollständiges `mapping`, nie beides) und `If-Match` mit der
`row_version` des Imports:

```bash title="cURL"
curl -sS -X PUT "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f/mapping" \
  -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" \
  -H 'If-Match: "1"' \
  -H "Content-Type: application/json" \
  -d '{"preset": "generic-csv", "options": {"facility_id": "0192f09e-4a5b-7c6d-9e8f-0a1b2c3d4e5f"}}'
```

Wurde der Import inzwischen geändert, lautet die Antwort `409 conflict`: Lesen Sie ihn mit
[`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import) neu und wiederholen Sie mit der neuen `row_version`.

## 4. Prüfen lassen und warten

[`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import) startet einen Auftrag, der jede Zeile mit den
Prüfregeln des Portals prüft und Seriennummern findet, die sich in der Datei wiederholen oder schon existieren.
Fragen Sie [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import) ab, bis `status` den Wert `validated` hat (oder
`failed`, wenn die Datei nicht gelesen werden konnte):

```bash title="cURL"
curl -sS -X POST "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f/validate" \
  -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY"

curl -sS "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \
  -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" | jq '{status, summary}'
```

```ts title="TypeScript"
const res = await fetch(`${BASE}/imports/${imp.id}/validate`, { method: 'POST', headers: auth });
if (res.status !== 202) throw new Error(`validateImport: ${res.status} ${(await res.json()).code}`);

let current = imp;
for (let attempt = 0; attempt < 120 && !['validated', 'failed'].includes(current.status); attempt++) {
  await new Promise((r) => setTimeout(r, 5000));
  current = await (await fetch(`${BASE}/imports/${imp.id}`, { headers: auth })).json();
}
// An die Fertigung melden: gültige Zeilen und Zeilen mit Warnungen werden importiert, Fehler und Duplikate nicht.
console.log(current.status, current.summary);
```

```python title="Python"
import time

res = session.post(f"{BASE}/imports/{imp['id']}/validate", timeout=30)
if res.status_code != 202:
    raise SystemExit(f"validateImport: {res.status_code} {res.json()['code']}")

current = imp
for _ in range(120):
    if current["status"] in ("validated", "failed"):
        break
    time.sleep(5)
    current = session.get(f"{BASE}/imports/{imp['id']}", timeout=30).json()
# An die Fertigung melden: gültige Zeilen und Zeilen mit Warnungen werden importiert, Fehler und Duplikate nicht.
print(current["status"], current.get("summary"))
```

```json title="200 (gekürzt)"
{
  "status": "validated",
  "summary": { "rows": 2, "valid": 2, "warnings": 0, "errors": 0, "duplicates": 0, "created": 0, "approved": 0, "not_approved": 0 }
}
```

Markieren Sie die Outbox-Zeilen als erledigt, sobald der Import geprüft ist, und senden Sie die Zusammenfassung an
die Fertigung. Zeilen mit Fehlern oder Duplikaten werden nicht importiert; Mitglieder sehen jede mit Zeile, Spalte
und Grund im Portal.

## 5. Mitglieder übernehmen, geben frei und veröffentlichen

Ab hier geschieht die Arbeit im Portal, nie mit einem Schlüssel:

1. Eine Bearbeiterin oder ein Bearbeiter öffnet den Import, liest den Prüfbericht und **übernimmt** ihn. Die
   Einheiten entstehen als Entwürfe, ihre Werte tragen die Quelle `import`.
2. Eine Prüferin oder ein Prüfer **gibt** die importierten Werte in einem Schritt **frei**. Werte mit einer
   fehlschlagenden Prüfung, ohne geforderten Nachweis oder mit einer Importwarnung bleiben zur Einzelprüfung offen;
   mit eingeschaltetem Vier-Augen-Prinzip auch Werte, die die prüfende Person selbst erfasst hat.
3. Eine Prüferin oder ein Prüfer **veröffentlicht** die Pässe, sobald keine Pflichtangabe mehr fehlt.

Der [Änderungsfeed](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp) meldet `import.committed`,
`battery.created`, `import.approved` und später `passport.published`; das ERP erfährt so von jedem Schritt, ohne die
Menschen zu fragen.

## Fehlerbehandlung

| Antwort                   | Bedeutung                                                                                                                              | Was tun                                                                                                                                                                              |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400 validation_error`    | Feld `file` (leer, weder CSV noch XLSX), `preset` (unbekannte Vorlage), `mapping` (keine Seriennummernspalte oder kein Weg zum Modell) | Datei oder Vorlage korrigieren; nicht unverändert wiederholen.                                                                                                                       |
| `401 unauthenticated`     | Schlüssel fehlt, ist unbekannt oder widerrufen                                                                                         | Geheimnis prüfen; Alarm auslösen.                                                                                                                                                    |
| `403 forbidden`           | Dem Schlüssel fehlt der Scope `import`                                                                                                 | Den Import-Schlüssel verwenden.                                                                                                                                                      |
| `404 not_found`           | Den Import gibt es in Ihrer Organisation nicht                                                                                         | Einen neuen Import beginnen.                                                                                                                                                         |
| `409 conflict`            | Der Import wurde geändert (`If-Match`) oder wird gerade geprüft oder übernommen                                                        | Import lesen und entscheiden.                                                                                                                                                        |
| `413 payload_too_large`   | Die Datei überschreitet die Upload-Grenze                                                                                              | Die Charge aufteilen.                                                                                                                                                                |
| `429 rate_limited`        | Zu viele Anfragen                                                                                                                      | `Retry-After` Sekunden warten.                                                                                                                                                       |
| `5xx`, Zeitüberschreitung | Ergebnis unbekannt                                                                                                                     | Mit Backoff wiederholen. Ein wiederholter Upload legt einen zweiten Import an, dessen Zeilen zu Duplikaten werden, sobald der erste übernommen ist; Mitglieder übernehmen nur einen. |

Der Status `failed` bedeutet, dass die Datei bei der Prüfung nicht gelesen werden konnte: Laden Sie eine
korrigierte Datei als neuen Import hoch.
