# tapinomahub Marktdatenimport für Demontagebetriebe und Verwerter

- **Version:** 1.0.0
- **Status:** Übergabefertiger Datenvertrag
- **Format:** JSON, UTF-8
- **Schema:** [recycler-market-data-v1.schema.json](./recycler-market-data-v1.schema.json)
- **Vollständiges Beispiel:** [recycler-market-data-v1.example.json](./recycler-market-data-v1.example.json)

## 1. Zweck

Dieser Datenvertrag beschreibt den Austausch von Bestands-, Angebots- und Verkaufsdaten
zwischen Demontagebetrieben beziehungsweise Fahrzeugverwertern und tapinomahub.

Das Format ermöglicht insbesondere:

- Marktanalysen je exakter OE-Nummer und Austauschbarkeitsgruppe,
- Angebots-, Preis- und Absatzanalysen je physischem Teil,
- Verkaufswahrscheinlichkeit und Standzeit nach 30, 90, 180 oder 365 Tagen,
- Erlös- und Verwertungsanalysen je Spenderfahrzeug,
- die Unterscheidung zwischen echten Verkäufen, ausgelaufenen Listings, Stornos und
  Retouren,
- kanalübergreifende Auswertungen ohne Mehrfachzählung desselben physischen Teils.

Der Vertrag trennt deshalb fünf fachliche Kernobjekte:

| Objekt | Bedeutung |
|---|---|
| `vehicle` | Ein konkretes Spenderfahrzeug |
| `part` | Ein konkretes physisches Teil, ein Satz, Kit oder eindeutig definierter Posten |
| `listing` | Ein Inserat dieses Teils auf genau einem Verkaufskanal |
| `listing_event` | Eine unveränderliche Preis- oder Statusänderung eines Listings |
| `sale` | Ein Verkauf mit einem oder mehreren eindeutig zugeordneten Teilen |

Zeitbezogene Messpunkte, eingebettete Verkaufsereignisse und Tombstones ergänzen diese
Kernobjekte als eigene Vertragsteile.

Ein physisches Teil kann gleichzeitig in mehreren Listings angeboten werden. Diese
Listings verwenden dieselbe `source_part_id`, aber jeweils eine andere
`source_listing_id`.

## 2. Verbindlichkeit

Die Wörter **MUSS**, **DARF NICHT**, **SOLL** und **KANN** sind normativ gemeint:

- **MUSS**: Voraussetzung für einen vertragskonformen Import.
- **DARF NICHT**: führt zu einem Importfehler.
- **SOLL**: fachlich dringend empfohlen; Abweichungen sind zu begründen.
- **KANN**: optionale Erweiterung mit zusätzlichem Analysewert.

Das JSON Schema ist für Struktur, Datentypen, Pflichtfelder und Enum-Werte maßgeblich.
Die zusätzlichen fachlichen Regeln in diesem Dokument gelten auch dann, wenn sie sich
nicht vollständig durch JSON Schema ausdrücken lassen.

## 3. Lieferumfang

Jeder Export ist genau ein JSON-Dokument mit diesen Arrays:

```json
{
  "schema_version": "1.0.0",
  "export": {},
  "vehicles": [],
  "parts": [],
  "listings": [],
  "listing_events": [],
  "listing_metrics": [],
  "sales": [],
  "tombstones": []
}
```

Alle Arrays MÜSSEN vorhanden sein, dürfen aber leer sein. Ein fehlendes Array ist nicht
gleichbedeutend mit „keine Daten vorhanden“ und wird deshalb abgelehnt.

Empfohlener Dateiname:

```text
tapinoma-market-data_<partner-id>_<YYYYMMDDTHHMMSSZ>_<export-id>.json
```

Die aktuelle Übergabe erfolgt als unkomprimierte `.json`-Datei per E-Mail an
`info@gobecom.com`. tapinomahub prüft und importiert die Lieferung intern. Pro Lieferung
sind höchstens 5.000 **normalisierte Arbeitseinheiten** vorgesehen. Dazu zählen alle
Datensätze der sieben Root-Arrays, alle eingebetteten `sales[].events` sowie jede Position
in `parts[].oe_numbers`, `vehicles[].economics`, `parts[].economics`, `listings[].items`,
`sales[].items` und `sales[].events[].items`.

Vor der ersten Lieferung werden die feste `partner_id` und der Schutzbedarf abgestimmt.
Größere Dateien oder sensible Inhalte werden als verschlüsselter Anhang beziehungsweise
über einen gesondert vereinbarten Übertragungsweg gesendet; ein Passwort wird auf einem
getrennten Kanal übermittelt. Ein optionaler SHA-256-Prüfwert kann als separate
`.sha256`-Datei mitgeliefert werden und bezieht sich auf die tatsächlich übertragene Datei.

## 4. Technische Grundregeln

### 4.1 Kodierung und Feldnamen

- Das Dokument MUSS valides UTF-8-JSON sein.
- Feldnamen und Enum-Werte sind Englisch und werden exakt wie vorgegeben geschrieben.
- Unbekannte zusätzliche Felder werden wegen `additionalProperties: false` abgelehnt.
- Quellspezifische Erweiterungen werden erst in einer späteren Schema-Version oder nach
  beidseitiger Abstimmung aufgenommen.

### 4.2 Fehlende und unbekannte Werte

- Unbekannte optionale Werte werden weggelassen.
- Bei ausdrücklich als nullable definierten Pflichtfeldern MUSS `null` verwendet werden.
- `0` ist immer ein echter Wert und DARF NICHT „unbekannt“ bedeuten.
- Leere Zeichenketten, `"-"`, `"n/a"` und erfundene Ersatzwerte sind unzulässig.
- Daten dürfen nicht geschätzt werden, außer ein Feld oder ein Kostenwert ist ausdrücklich
  als `estimated` gekennzeichnet.

### 4.3 Zeitangaben

- Zeitpunkte MÜSSEN RFC-3339-Zeitstempel mit `Z` oder explizitem UTC-Offset sein.
- Reine Datumsfelder verwenden `YYYY-MM-DD`.
- Bevorzugt wird UTC, zum Beispiel `2026-09-08T08:00:00Z`.
- `occurred_at` ist der fachliche Ereigniszeitpunkt im Quellsystem.
- `updated_at` ist der Zeitpunkt der letzten Änderung dieses Quelldatensatzes.
- Import- oder Exportzeitpunkt dürfen einen unbekannten Fachzeitpunkt nicht ersetzen.

### 4.4 Geldbeträge

Geldbeträge werden ohne Fließkommazahlen in der kleinsten Währungseinheit übertragen:

```json
{
  "amount_minor": 5990,
  "currency": "EUR",
  "price_basis": "gross",
  "tax_treatment": "margin_scheme"
}
```

Bei EUR bedeutet `5990` exakt `59,90 EUR`.

Für jeden Betrag gelten folgende Regeln:

- `amount_minor` MUSS eine nicht negative Ganzzahl sein.
- `currency` MUSS ein ISO-4217-Währungscode in Großbuchstaben sein.
- `price_basis` MUSS `gross`, `net` oder `unknown` sein.
- `tax_treatment` SOLL geliefert werden, wenn die Information vorliegt.
- Preis, Versand, Rabatt, Gebühr und Erstattung werden getrennt übertragen.
- Eine Erstattung wird als positiver Betrag mit passendem Verkaufsereignis übertragen,
  nicht als negativer Verkaufspreis.

Werden zwei Geldbeträge fachlich verglichen, müssen neben der Währung auch
`price_basis` und ein bekanntes `tax_treatment` kompatibel sein. Dies gilt insbesondere
für initialen und letzten Listingpreis, Preisereignisse eines Listings sowie
Erstattungsbeträge auf Verkaufs- und Teileebene. `unknown` beziehungsweise ein fehlendes
`tax_treatment` bedeutet fehlendes Wissen und wirkt bei dieser Prüfung als Wildcard; zwei
voneinander abweichende bekannte Werte sind dagegen ein harter Fehler.

### 4.5 Länder

Länder werden als ISO-3166-1-Alpha-2-Code in Großbuchstaben übertragen, zum Beispiel
`DE`, `AT` oder `NL`.

## 5. Identitäten und Verknüpfungen

### 5.1 Namensraum

Alle `source_*_id`-Werte werden vom Datenlieferanten vergeben. Sie MÜSSEN innerhalb einer
`partner_id` dauerhaft eindeutig sein und dürfen niemals für ein anderes Objekt
wiederverwendet werden.

Der globale Schlüssel ist damit beispielsweise:

```text
partner_id + source_part_id
```

Eine erneute Übertragung derselben ID ist ein Upsert und darf kein Duplikat erzeugen.

### 5.2 Bedeutung der IDs

| Feld | Bedeutung |
|---|---|
| `source_vehicle_id` | Stabile Quell-ID genau eines Spenderfahrzeugs |
| `source_part_id` | Stabile Quell-ID einer physischen Verkaufseinheit |
| `inventory_sku` | Sichtbare Lager-/Artikelnummer; darf mit `source_part_id` identisch sein |
| `source_listing_id` | Stabile ID genau einer Veröffentlichungsinstanz auf einem Kanal |
| `source_event_id` | Stabile ID eines unveränderlichen Listing-Ereignisses |
| `source_metric_id` | Stabile ID einer Messung von Listing-Nachfragesignalen |
| `source_sale_id` | Stabile ID eines Verkaufs beziehungsweise Auftrags |
| `source_sale_event_id` | Stabile ID eines Verkaufs-, Storno- oder Retourenereignisses |

OE-Nummer, VIN, Lager-SKU und Listing-ID dürfen nicht als austauschbare Identitäten
behandelt werden:

- Viele physische Teile können dieselbe OE-Nummer haben.
- Ein Fahrzeug kann viele physische Teile liefern.
- Ein Teil kann mehrere Listings haben.
- Ein Listing kann ein Bundle aus mehreren Teilen enthalten.

## 6. Exporthülle

### 6.1 Pflichtfelder

| Feld | Bedeutung |
|---|---|
| `export.export_id` | Unveränderliche ID genau dieser Exportdatei |
| `export.mode` | `full_snapshot` oder `delta` |
| `export.exported_at` | Erzeugungszeitpunkt der Datei |
| `export.source.partner_id` | Stabile ID des Datenlieferanten |
| `export.source.source_system` | Bezeichnung des Quellsystems |
| `export.coverage` | Vollständigkeit der fachlichen Datenbereiche |

Der Inhalt zu einer bereits verwendeten Kombination aus `partner_id` und `export_id`
MUSS bei jeder erneuten Lieferung identisch sein. Eine Wiederholung ist zulässig und muss
idempotent verarbeitet werden. Dieselbe ID mit verändertem Inhalt ist ein harter Fehler.

### 6.2 Abdeckungsangaben

Für jeden Bereich wird angegeben:

- `complete`: Alle im vereinbarten Umfang vorhandenen Daten sind enthalten.
- `partial`: Nur ein bekannter Ausschnitt ist enthalten.
- `unknown`: Die Vollständigkeit kann nicht bestätigt werden.

Dies gilt getrennt für Fahrzeugbestand, Teilebestand, Listings, Listing-Historie,
Verkäufe und Retouren. Eine unvollständige Listing-Historie ist kein Importfehler, muss
aber als `partial` deklariert werden.

Sobald `listing_history` oder `returns` als `complete` deklariert wird, MUSS
`history_available_from` als nicht-leerer Zeitstempel geliefert werden. `complete`
bedeutet in diesen beiden Bereichen vollständig **ab diesem Zeitpunkt**, nicht zwingend
seit Entstehung des Quellsystems. Der Zeitpunkt darf nicht nach dem Liefer-Stichtag liegen.

## 7. Vollständiger Snapshot und Delta

### 7.1 `full_snapshot`

Ein vollständiger Snapshot bildet den aktuellen Zustand des ausdrücklich angegebenen
`scope` zum Zeitpunkt `snapshot_at` ab.

Bei `full_snapshot` sind zusätzlich Pflicht:

- `snapshot_at`,
- `scope.source_location_ids`,
- `scope.source_channel_ids`,
- `scope.entity_types`,
- `record_counts` für alle Arrays.

Die Zählwerte MÜSSEN exakt den Array-Längen der Datei entsprechen. Ein Snapshot darf erst
nach erfolgreicher Struktur-, Referenz- und Fachvalidierung aktiviert werden.
`record_counts.sale_events` zählt zusätzlich die Summe aller verschachtelten
`sales[].events`; damit ist auch die gelieferte Verkaufs-, Retouren- und
Erstattungshistorie vom Zählvertrag erfasst.

Das Fehlen eines zuvor bekannten Datensatzes in einem Snapshot bedeutet ausschließlich:
„im angegebenen Scope nicht mehr vorhanden“. Es bedeutet niemals automatisch verkauft,
verschrottet oder gelöscht. Ohne expliziten Status beziehungsweise Tombstone bleibt der
fachliche Abgangsgrund unbekannt.

### 7.2 `delta`

Ein Delta enthält alle innerhalb des Zeitfensters neu angelegten oder geänderten
Datensätze sowie neu entstandene Ereignisse.

Bei `delta` sind Pflicht:

- `period_from`,
- `period_to`.

Das Intervall wird als `(period_from, period_to]` verstanden: Start exklusiv, Ende
inklusive. Aufeinanderfolgende Deltas SOLLEN lückenlos anschließen. `sequence` SOLL
monoton steigen, damit Lücken erkannt werden können.

Jede enthaltene aktuelle Entität wird als vollständiger aktueller Stand übertragen, nicht
als unvollständiger Feld-Patch. Ereignisse bleiben append-only.

Ein Fremdschlüssel darf auf einen Datensatz im selben Export oder auf einen bereits früher
erfolgreich importierten Datensatz desselben Partners zeigen.

Verspätete Deltas dürfen anhand von `updated_at` beziehungsweise `occurred_at` historisch
einsortiert werden, aber keinen neueren aktuellen Zustand zurücksetzen.

## 8. Spenderfahrzeug `vehicles`

### 8.1 Pflichtfelder

| Feld | Bedeutung |
|---|---|
| `source_vehicle_id` | Stabile Quell-ID des Fahrzeugs |
| `source_location_id` | Betriebs-/Standort-ID |
| `vin` | Vollständige VIN in Großbuchstaben |
| `status` | Aktueller Fahrzeugstatus |
| `status_changed_at` | Zeitpunkt des aktuellen Status |
| `acquired_at` | Fahrzeugzugang; bei historischen Altdaten ausdrücklich `null` |
| `dismantled_at` | Abschluss der Demontage; solange nicht erfolgt `null` |
| `mileage_km` | Kilometerstand beim Zugang; unbekannt ausdrücklich `null` |
| `damage.overall_condition` | Grobe Zustands-/Schadensklasse |
| `updated_at` | Letzte Änderung im Quellsystem |

Die VIN MUSS 17 Zeichen lang sein und darf die Zeichen `I`, `O` und `Q` nicht enthalten.
Teile ohne gültige VIN gehören nicht in das analytische Pflichtprofil dieser Version und
müssen vor dem Export geklärt oder separat behandelt werden.

### 8.2 Fahrzeugstatus

Zulässige Werte:

- `received`
- `in_dismantling`
- `dismantled`
- `sold_complete`
- `recycled`
- `disposed`
- `unknown`

### 8.3 Empfohlene Fahrzeugmerkmale

Zusätzlichen Analysewert liefern:

- Hersteller, Modell, Baureihe, Variante und Plattform,
- Modelljahr, Produktionsdatum und Erstzulassung,
- HSN, TSN, K-Type-IDs und NAT-Codes,
- Motorcode, Kraftstoff, Leistung und Hubraum,
- Getriebecode, Getriebeart und Gangzahl,
- Antrieb, Karosserie und Ursprungsmarkt,
- fahrzeugspezifische Ausstattungs-/PR-/SA-Codes,
- Schadenzonen, Fahrbereitschaft und Motorlauf,
- Einkaufspreis, Transport-, Entsorgungs- und Schrotterlöse.

Produktionsdatum, Modelljahr und Erstzulassung sind verschiedene Merkmale und dürfen nicht
gegenseitig ersetzt werden. Angaben des Datenlieferanten bleiben als Quellwerte erhalten;
tapinomahub kann sie anschließend per VIN anreichern oder normalisieren.

### 8.4 Wirtschaftsdaten `economics`

`vehicles[].economics` und `parts[].economics` erfassen Kosten und Erlöse, die nicht in
Listing- oder Verkaufsbeträgen stecken. Jeder Eintrag enthält:

| Feld | Bedeutung |
|---|---|
| `type` | Kosten-/Erlösart, zum Beispiel `vehicle_acquisition`, `inbound_transport`, `dismantling`, `testing`, `storage`, `disposal` oder `scrap_revenue` |
| `direction` | `cost` oder `revenue` |
| `amount` | Geldbetrag gemäß Abschnitt 4.4 |
| `valuation_method` | `actual`, `allocated`, `estimated` oder `unknown` |
| `occurred_at` | Optionaler fachlicher Zeitpunkt; unbekannt weglassen oder `null` |
| `note` | Optionale sachliche Erläuterung ohne personenbezogene Daten |

Ein Betrag darf entweder auf Fahrzeug- oder auf Teileebene ausgewiesen werden, aber nicht
auf beiden Ebenen doppelt in dieselbe Deckungsbeitragsrechnung eingehen. Gemeinkosten, die
auf Teile verteilt werden, tragen `valuation_method: "allocated"`; reine Schätzwerte
`estimated`. Verkaufspreise, Käufer-Versand, Plattformgebühren und Erstattungen gehören
weiterhin in `sales` und nicht als zweite Kopie in `economics`.

## 9. Physisches Teil `parts`

### 9.1 Pflichtfelder

| Feld | Bedeutung |
|---|---|
| `source_part_id` | ID des konkreten Teils, Satzes, Kits oder Postens |
| `source_location_id` | Standort des Bestands |
| `source_vehicle_id` | Verweis auf das Spenderfahrzeug |
| `inventory_sku` | Lager-/Artikelnummer; darf mit der Teile-ID identisch sein |
| `status` | Aktueller Bestandsstatus des physischen Teils |
| `status_changed_at` | Zeitpunkt des aktuellen Status |
| `oe_numbers` | Mindestens eine strukturierte OE-Nummer |
| `name` | Quellbezeichnung des Teils |
| `category.source_name` | Quellkategorie |
| `quantity` | Zahl der Einheiten dieses Bestandsobjekts |
| `unit_type` | `single`, `set`, `kit` oder `lot` |
| `condition` | Zustands-, Prüf- und Vollständigkeitsdaten |
| `removed_at` | Demontagezeitpunkt; vor Demontage `null` |
| `updated_at` | Letzte Änderung im Quellsystem |

Für gebrauchte Einzelteile SOLL `quantity` gleich `1` sein. Mehrere identische Teile aus
unterschiedlichen Fahrzeugen MÜSSEN getrennte `source_part_id`-Werte erhalten. Wenn ein
Quellsystem nur einen aggregierten Posten kennt, wird `unit_type: "lot"` verwendet und
`quantity_aggregated` als Datenqualitätsflag gesetzt.

### 9.2 Teilestatus

Zulässige Werte:

- `on_vehicle`
- `removed`
- `available`
- `listed`
- `reserved`
- `sold`
- `returned`
- `recycled`
- `disposed`
- `missing`
- `unknown`

Der Teilestatus beschreibt den physischen Bestand. Der Listing-Status beschreibt nur ein
einzelnes Angebot. Zwei aktive Listings desselben Teils erhöhen den physischen Bestand
nicht.

### 9.3 OE-Nummern

Jede OE-Nummer enthält:

| Feld | Bedeutung |
|---|---|
| `raw` | Unveränderter Wert aus Etikett oder Quellsystem |
| `normalized` | Optionaler normalisierter Wert |
| `manufacturer` | Herausgeber beziehungsweise OE-Nummernraum |
| `relation` | Beziehung zum vorliegenden Teil |
| `evidence` | Quelle der Zuordnung |
| `confidence` | Optionale Sicherheit von `0` bis `1` |

Pro Teil MUSS genau eine OE-Nummer `relation: "primary"` tragen. Weitere Beziehungen sind:

- `alternative`
- `supersedes`
- `superseded_by`
- `interchangeable`

Der Rohwert MUSS immer erhalten bleiben. Bei der Normalisierung dürfen insbesondere
führende Nullen, Suffixe und Revisionskennungen nicht verloren gehen. Wenn der Lieferant
keine gesicherte Normalisierung hat, wird `normalized` weggelassen und tapinomahub
normalisiert zentral.

Nicht-OE-Nummern wie Aftermarket-, Lieferanten-, Barcode- oder interne Nummern gehören in
`reference_numbers`, nicht in `oe_numbers`.

### 9.4 Zustand

Pflichtwerte:

- `condition_code`: `new`, `used`, `refurbished`, `defective`, `for_parts` oder `unknown`
- `test_status`: `passed`, `failed`, `partial`, `not_tested` oder `unknown`
- `completeness`: `complete`, `incomplete` oder `unknown`

Optionale Qualitätsklassen werden einheitlich interpretiert:

| Klasse | Definition |
|---|---|
| `A` | geprüft, vollständig, funktionsfähig, höchstens geringe Gebrauchsspuren |
| `B` | funktionsfähig, normale sichtbare Gebrauchsspuren |
| `C` | deutliche Gebrauchsspuren oder reparabler Mangel, genau beschrieben |
| `D` | defekt beziehungsweise nur als Teileträger geeignet |
| `unknown` | keine belastbare Einstufung vorhanden |

Ein fehlender Prüfstatus darf nicht als bestandene Prüfung interpretiert werden. Konkrete
Defekte werden zusätzlich als einzelne Einträge in `defects` übertragen.

### 9.5 Einbauposition und Fitments

Links/rechts, vorne/hinten und der Einbaubereich werden strukturiert in `position`
übertragen. Richtungsangaben beziehen sich immer auf die normale Fahrtrichtung.

Das Spenderfahrzeug beweist genau eine konkrete Verwendung. Eine allgemeine
Fahrzeugkompatibilität darf daraus nicht ungeprüft abgeleitet werden. Optionale `fitments`
enthalten deshalb immer Quelle und Konfidenz.

## 10. Listing `listings`

### 10.1 Pflichtfelder

| Feld | Bedeutung |
|---|---|
| `source_listing_id` | ID genau dieser Veröffentlichungsinstanz |
| `source_location_id` | Zuständiger Lager-/Verkaufsstandort |
| `items` | Zugeordnete physische Teile und Mengen |
| `channel` | Kanal-ID, Kanaltyp, Name und Marktland |
| `listed_at` | Tatsächliche Erstveröffentlichung; falls unbekannt `null` |
| `first_seen_at` | Erste sichere Beobachtung durch das Quellsystem |
| `last_seen_at` | Letzte sichere Beobachtung |
| `status` | Aktueller Listing-Status |
| `status_changed_at` | Zeitpunkt des aktuellen Status |
| `status_reason` | Bei aktivem Listing `null`, bei beendetem Listing Abgangsgrund |
| `initial_ask_price` | Ursprünglicher Einstellpreis |
| `last_ask_price` | Zuletzt gültiger Einstellpreis |
| `shipping_price` | Versandpreis; unbekannt oder nicht separat verfügbar `null` |
| `updated_at` | Letzte Änderung im Quellsystem |

`listed_at` und `first_seen_at` dürfen nur dann gleichgesetzt werden, wenn der tatsächliche
Veröffentlichungszeitpunkt bekannt ist. Bei historischen Listings mit unbekanntem Beginn
ist `listed_at: null` korrekt; `first_seen_at` bleibt der erste belegte Zeitpunkt.

### 10.2 Listing-Status

Zulässige Werte:

- `draft`
- `active`
- `paused`
- `reserved`
- `sold`
- `withdrawn`
- `expired`
- `scrapped`
- `unknown`

Für `sold`, `withdrawn`, `expired` und `scrapped` MUSS `status_reason` einen konkreten Wert
tragen. Zulässige Gründe sind unter anderem:

- `sold_on_channel`
- `sold_elsewhere`
- `expired`
- `withdrawn_by_seller`
- `damaged`
- `scrapped`
- `duplicate`
- `channel_policy`
- `missing_from_source`
- `unknown`

Ein verschwundenes Listing ist nicht automatisch verkauft. Kann der Abgang nicht geklärt
werden, wird `missing_from_source` oder `unknown` verwendet.

### 10.3 Mehrfachlisting, Relisting und Bundles

- Mehrere Kanäle desselben Teils erhalten jeweils eigene Listing-IDs.
- Nach Verkauf auf einem Kanal werden parallele Listings explizit mit
  `sold_elsewhere` geschlossen.
- Eine neue Veröffentlichungsinstanz erhält eine neue ID und verweist optional mit
  `relist_of_listing_id` auf das frühere Listing.
- Der Relist-Ursprung MUSS anhand von `listed_at`, ersatzweise `first_seen_at`, strikt
  früher als die neue Veröffentlichungsinstanz liegen. Relist-Verweise dürfen keine
  Zyklen bilden.
- Ein Bundle enthält mehrere `items`; ein Set oder Kit kann auch als ein physisches
  Bestandsobjekt modelliert sein.
- Ein Listing darf eine Teilemenge nicht überschreiten.

## 11. Listing-Ereignisse `listing_events`

Ab Integrationsbeginn MUSS jede Preis- und relevante Statusänderung als unveränderliches
Ereignis geliefert werden. Historische Ereignisse vor Integrationsbeginn können fehlen,
wenn `export.coverage.listing_history` als `partial` oder `unknown` gekennzeichnet ist.

Pflichtfelder jedes Ereignisses:

- `source_event_id`
- `source_listing_id`
- `type`
- `occurred_at`

Zulässige Ereignistypen:

- `listed`
- `relisted`
- `price_changed`
- `quantity_changed`
- `reserved`
- `reservation_released`
- `paused`
- `reactivated`
- `sold`
- `withdrawn`
- `expired`
- `scrapped`
- `corrected`

Für `listed`, `relisted` und `price_changed` ist `ask_price` Pflicht. Ein `sold`-Ereignis
verweist über `source_sale_id` auf den Verkauf. Ein Korrekturereignis verweist mit
`corrects_event_id` auf das korrigierte Ereignis und enthält eine Begründung.

Ein bereits übertragenes Ereignis darf unter derselben ID nicht verändert werden. Ein
`corrected`-Ereignis ist ausschließlich ein Korrekturmarker: Es erklärt das über
`corrects_event_id` referenzierte Ereignis für fachlich unwirksam und verändert selbst
weder Preis noch Menge noch Status. Deshalb darf der Marker weder `ask_price`, `quantity`,
`end_reason` noch `source_sale_id` enthalten. Der richtige Fachzustand wird unter einer
neuen stabilen `source_event_id` geliefert. Nur ein Ereignis vom Typ `corrected` darf
`corrects_event_id` tragen.

Ein Korrekturmarker darf sich nicht selbst referenzieren. Das korrigierte Ereignis MUSS
zum selben Listing gehören und anhand von `occurred_at` strikt früher liegen. Nur ein
technisch falscher Quelldatensatz wird stattdessen durch einen Tombstone entfernt.

## 12. Nachfragesignale `listing_metrics`

Dieses Array darf leer sein. Wenn der Verkaufskanal die Daten liefert, können folgende
Signale übertragen werden:

- Impressionen,
- Detailaufrufe,
- Beobachter/Merkliste,
- Anfragen,
- Angebote,
- Warenkorbzugänge.

`measurement_type: "cumulative"` bezeichnet den kumulierten Stand zum Zeitpunkt
`observed_at`. `measurement_type: "period"` bezeichnet genau das Intervall von
`period_from` bis `period_to`; beide Grenzen sind dann Pflicht.

Metriken unterschiedlicher Plattformen dürfen nicht ohne Kennzeichnung als identische
Messgrößen interpretiert werden. Der zugehörige Kanal ergibt sich aus dem referenzierten
Listing.

## 13. Verkäufe `sales`

### 13.1 Pflichtfelder

| Feld | Bedeutung |
|---|---|
| `source_sale_id` | Stabile Verkaufs-/Auftrags-ID |
| `source_location_id` | Zuständiger Standort |
| `source_listing_id` | Zugehöriges Listing; bei Offlineverkauf weglassen |
| `channel` | Tatsächlicher Verkaufskanal |
| `sold_at` | Zeitpunkt der verbindlichen Annahme im Quellsystem |
| `status` | Aktueller Verkaufsstatus |
| `status_changed_at` | Zeitpunkt des aktuellen Status |
| `items` | Verkaufte physische Teile, Mengen und tatsächlich vereinbarte Teilepreise |
| `shipping_charged` | Dem Käufer berechneter Versand oder `null` |
| `platform_fee` | Plattformgebühr oder `null` |
| `payment_fee` | Zahlungsgebühr oder `null` |
| `refund_amount` | Bisher erstatteter Gesamtbetrag oder `null` |
| `events` | Verkaufs-, Storno- und Retourenverlauf; darf bei Altdaten leer sein |
| `updated_at` | Letzte Änderung im Quellsystem |

Der `item_amount` einer Verkaufsposition ist der tatsächlich vereinbarte Teilepreis nach
Artikelrabatten, aber ohne Versand. Bei einem Bundle MUSS der Teilepreis nachvollziehbar
auf die enthaltenen physischen Teile verteilt werden. Ist keine sachgerechte Allokation
möglich, muss dies vor der Übertragung abgestimmt werden; eine willkürliche Aufteilung ist
unzulässig.

### 13.2 Verkaufsstatus

Zulässige Werte:

- `pending`
- `paid`
- `fulfilled`
- `completed`
- `cancelled`
- `partially_returned`
- `returned`
- `partially_refunded`
- `refunded`

Storno, Retoure und Erstattung verändern den aktuellen Status, löschen aber weder den
ursprünglichen Verkauf noch seine Ereignisse. Ein retourniertes Teil wird nur durch eine
zusätzliche Bestandsänderung wieder verfügbar; eine Retoure allein darf den Teilebestand
nicht automatisch reaktivieren.

Für Analysen werden Käuferdaten auf `buyer_segment` und `buyer_country` beschränkt.
Personennamen, E-Mail-Adressen, Telefonnummern und genaue Käuferadressen sind nicht Teil
dieses Vertrags.

### 13.3 Teilebezogene Retouren- und Erstattungsereignisse

Für die Ereignistypen `return_opened`, `partially_returned`, `returned`,
`partially_refunded` und `refunded` ist ein nicht leeres `items`-Array Pflicht. Jede
Ereignisposition enthält:

| Feld | Bedeutung |
|---|---|
| `source_part_id` | Exakt eine Position aus `sale.items` |
| `quantity` | Kumulierter Mengenstand des unten beschriebenen Ereignisverlaufs |
| `amount` | Optionaler, diesem Teil kumuliert zugeordneter Erstattungsbetrag |

Die Mengen werden in drei voneinander getrennten Verläufen geführt:

| Ereignistyp | Kumulative Bedeutung von `items[].quantity` |
|---|---|
| `return_opened` | Bis dahin zur Rückgabe angemeldete Menge |
| `partially_returned`, `returned` | Bis dahin physisch zurückerhaltene Menge |
| `partially_refunded`, `refunded` | Bis dahin von einer Erstattung betroffene Menge |

Jedes Ereignis enthält den **vollständigen kumulierten Stand seines Verlaufs**, nicht nur
die Änderung seit dem vorherigen Ereignis. Eine zuvor enthaltene Teileposition MUSS daher
in jedem späteren Ereignis desselben Verlaufs wiederholt werden. Je Teileposition darf die
Menge, chronologisch nach `occurred_at` ausgewertet, nur gleich bleiben oder steigen. Die
drei Verläufe werden nicht untereinander verglichen: Eine angemeldete Rückgabemenge ist
nicht automatisch bereits physisch eingegangen oder erstattet.

`items[].amount` bezeichnet immer die bis zu diesem Zeitpunkt tatsächlich kumuliert auf
die Teileposition entfallende Erstattung, niemals eine erwartete Gutschrift und niemals
einen Delta-Betrag. Das Feld KANN fehlen, wenn das Quellsystem keine sachgerechte
Teileallokation liefert. Sobald ein Betrag geliefert wird, darf er in späteren
Retouren-/Erstattungsereignissen nicht sinken. Er verwendet dieselbe Währung wie
`sale.items[].item_amount` und darf diesen vereinbarten Teilepreis nicht überschreiten.

`sale.events[].amount` ist der kumulierte Gesamterstattungsbetrag an den Käufer zum
Ereigniszeitpunkt. Für `partially_refunded` und `refunded` ist er Pflicht und darf nicht
`null` sein; bei den drei Rückgabeereignissen ist er optional. Er kann zusätzlich
erstattete Versandkosten enthalten. Die Summe der bekannten `items[].amount` darf ihn
nicht überschreiten. Ereignisbetrag und `sale.refund_amount` verwenden die
Verkaufswährung, dürfen die Summe aus Teilepreisen und berechnetem Versand nicht
überschreiten und dürfen chronologisch nicht sinken. Der aktuelle Stand in
`sale.refund_amount` darf nicht kleiner als ein bereits gemeldeter kumulierter Ereignis-
oder Teilebetrag sein.

Bei `coverage.returns = complete` gleicht der Import den aktuellen
`sale.refund_amount` mit der gesamten gespeicherten und in der Lieferung enthaltenen
Ereignishistorie ab. Das gilt für Deltas und vollständige Snapshots. Bereits persistierte
oder aus Datenschutzgründen anonymisierte Ereignisse werden nicht erneut verlangt und
dürfen wegen ihres Tombstone-Watermarks auch nicht unter derselben ID neu angelegt
werden. Deshalb ist die persistierte Prüfung autoritativ; ein Sale darf in der JSON-Datei
ohne seine älteren Refund-Events erscheinen. Tatsächlich gelieferte Refund-Events
unterliegen weiterhin allen Betrags-, Mengen- und Monotonieprüfungen. Ein Verkauf mit
`status = completed` und positivem Erstattungsbetrag ist bei
`coverage.returns = complete` widersprüchlich; nach einer Erstattung ist der passende
Retouren-/Erstattungsstatus zu liefern.

Eine Ereignisposition MUSS zu `sale.items` gehören und darf deren verkaufte Menge nicht
überschreiten. Dieselbe `source_part_id` darf in einem Ereignis nur einmal vorkommen. Ein
technisch falsches Sale-Event wird mit einem Tombstone vom Typ `sale_event` korrigiert;
der fachlich richtige Verlauf wird anschließend unter einer neuen stabilen Ereignis-ID
geliefert.

## 14. Tombstones `tombstones`

Tombstones dienen ausschließlich der technischen Korrektur eines irrtümlich angelegten,
doppelten oder aus Datenschutzgründen zu entfernenden Quelldatensatzes.

Pflichtfelder:

- `entity_type`
- `source_id`
- `source_location_id`
- `source_channel_id`
- `deleted_at`
- `reason`

Zulässige `entity_type`-Werte sind `vehicle`, `part`, `listing`, `listing_event`,
`listing_metric`, `sale` und `sale_event`. Bei `sale_event` bezeichnet `source_id` die
`source_sale_event_id`.

`source_location_id` bezeichnet den Standort des zu korrigierenden Datensatzes. Für
`vehicle` und `part` MUSS `source_channel_id` explizit `null` sein. Für `listing`,
`listing_event`, `listing_metric`, `sale` und `sale_event` MUSS die stabile Kanal-ID
geliefert werden. In einem vollständigen Snapshot müssen Zielobjekttyp, Standort und –
soweit vorhanden – Kanal innerhalb des angegebenen `scope` liegen. `sale_event` wird
dabei dem Scope-Objekttyp `sales` zugeordnet.

Für `reason: "data_protection"` bleiben die Herkunftsfelder Pflicht, dürfen eine
datenschutzbedingte Löschung aber nicht blockieren: Im vollständigen Snapshot wird nur
geprüft, dass `tombstones` im Entity-Scope liegt. Der Import leitet Standort und Kanal
eines vorhandenen kontogebundenen Zielobjekts serverseitig ab und entfernt dessen
schutzbedürftige Inhalte auch bei veralteten Herkunftsangaben.

Verkauf, Retoure, Rückzug, Verschrottung oder ein nicht mehr sichtbares Listing sind
fachliche Zustände und dürfen nicht als Tombstone übertragen werden.
Dieselbe Quell-ID darf innerhalb einer Lieferung nicht zugleich als aktuelle Entität
beziehungsweise Ereignis und als Tombstone desselben `entity_type` enthalten sein.

## 15. Empfohlene Statusfolgen

Typischer Lebenszyklus eines physischen Teils:

```text
on_vehicle → removed → available → listed → reserved → sold
                                      ↑                  ↓
                                      └──── returned ────┘

removed / available / listed → recycled | disposed | missing
```

Typischer Listing-Lebenszyklus:

```text
draft → active → paused → active
           ↓        ↓
        reserved → active
           ↓
          sold

active | paused | reserved → withdrawn | expired | scrapped
```

Typischer Verkaufslebenszyklus:

```text
pending → paid → fulfilled → completed
    └──────────────→ cancelled
fulfilled | completed → partially_returned | returned
fulfilled | completed → partially_refunded | refunded
```

Abweichende Folgen können fachlich zulässig sein, müssen aber durch Ereignisse und Gründe
nachvollziehbar bleiben.

## 16. Fachliche Validierungsregeln

Neben dem JSON Schema gelten mindestens diese Prüfungen:

1. Alle IDs sind innerhalb des Partner-Namensraums eindeutig und stabil.
2. Alle Referenzen sind im selben Export oder im bereits importierten Partnerbestand
   auflösbar.
3. Jede `source_part_id` verweist auf genau ein Spenderfahrzeug.
4. Jede Teileinstanz enthält genau eine primäre OE-Nummer.
5. Gleiche normalisierte OE-Nummern werden innerhalb desselben Teils nicht doppelt geführt.
6. `first_seen_at` darf nicht nach `last_seen_at` liegen.
7. Ereigniszeiten werden chronologisch einsortiert; ältere Deltas überschreiben keinen
   neueren Status.
8. Innerhalb eines Listings bleiben Währung, bekannte Preisbasis und bekannte
   Steuerbehandlung über alle Preisereignisse kompatibel. Ein Währungswechsel erfordert
   ein neues Listing.
9. Ein bestätigter Onlineverkauf verweist auf ein Listing und dessen physische Teile.
10. Bei einem Offlineverkauf darf `source_listing_id` fehlen; der Verkauf verweist weiterhin
    über seine Positionen auf die Teile.
11. Verkaufte Mengen dürfen weder Listing- noch Bestandsmenge überschreiten.
12. Für ein nicht mengenfähiges Einzelteil darf es nicht gleichzeitig zwei wirksame
    Verkäufe geben.
13. `sold_at` darf bei Onlineverkäufen nicht vor `listed_at` liegen.
14. Ein `sold`-Ereignis muss einen passenden Verkaufsdatensatz referenzieren.
15. Status `sold` und Verkauf müssen fachlich konsistent sein; Konflikte werden nicht
    automatisch zugunsten einer Seite aufgelöst.
16. Bei kanalübergreifendem Verkauf werden andere aktive Listings mit
    `sold_elsewhere` beendet.
17. Kumulative Retouren- und Erstattungsmengen werden je Verlauf und Teileposition
    monoton geführt und dürfen die jeweilige Verkaufsmenge nicht übersteigen.
18. Kumulative Teile- und Gesamterstattungen werden monoton geführt, verwenden
    kompatible Geldmetadaten und dürfen weder Positionspreis noch belasteten
    Käuferbetrag überschreiten. Bei vollständiger Retourenabdeckung erfolgt der Abgleich
    des aktuellen Erstattungsbetrags autoritativ gegen die persistierte Gesamthistorie;
    ältere oder anonymisierte Events müssen nicht erneut in der Payload stehen.
    `completed` mit positiver Erstattung ist bei vollständiger Retourenabdeckung
    unzulässig.
19. `record_counts` eines Snapshots stimmen exakt mit den Array-Längen sowie der
    verschachtelten Summe `sale_events` überein.
20. Ein fehlender Datensatz oder ein fehlendes Listing gilt niemals automatisch als
    Verkauf.
21. `period_from` darf weder beim Export noch bei einer periodischen Metrik nach
    `period_to` liegen.
22. `sold_at`, alle `status_changed_at`-Werte, `listed_at`, `first_seen_at`,
    `last_seen_at`, `acquired_at`, `dismantled_at`, `removed_at`, `condition.tested_at`
    und alle `economics[].occurred_at` dürfen weder nach dem jeweiligen `updated_at` noch
    nach dem Liefer-Stichtag liegen. Für Deltas ist dieser Stichtag `period_to` (nur ohne
    Intervall ersatzweise `exported_at`), für vollständige Snapshots `snapshot_at`.
    `production_date` und `first_registration_date` dürfen nicht nach dessen Kalendertag
    liegen.
23. Korrekturereignisse und Relist-Verweise dürfen sich nicht selbst referenzieren oder
    Zyklen bilden; ihr Ursprung liegt zeitlich strikt vor dem neuen Datensatz.
24. Tombstones tragen den Standort und bei kanalgebundenen Objekten den Kanal des
    Zielobjekts. In Snapshots liegen Zieltyp, Standort und Kanal vollständig im Scope;
    für `data_protection` gilt die in Abschnitt 14 beschriebene Lösch-Ausnahme.
25. Eine Quell-ID darf in derselben Lieferung nicht gleichzeitig geliefert und
    tombstoned werden; dies gilt auch für eingebettete `sale_event`-IDs.

Eine Lieferung wird atomar verarbeitet: Sobald ein harter Fehler auftritt, wird kein
Teilstand freigeschaltet. Der Fehler wird mit einem maschinenlesbaren JSON-Pointer
gemeldet. Unauflösbare Referenzen, Doppelverkäufe, ungültige Preise und widersprüchliche
Mengen sind harte Fehler. Fehlende optionale Analysemerkmale sind Warnungen.

Empfohlenes Fehlerformat:

```json
{
  "code": "unknown_reference",
  "path": "/listings/3/items/0/source_part_id",
  "message": "Referenced source ID was not found for this account and partner.",
  "severity": "error"
}
```

Fehlerantworten enthalten aus Datenschutzgründen weder den beanstandeten Feldwert noch
den vollständigen Quelldatensatz. Je Antwort werden höchstens 100 Fehler ausgegeben.

## 17. Datenschutz und Sicherheit

- Vor der ersten Datenübertragung MÜSSEN Absender, Empfänger, Rechtsgrundlage,
  Aufbewahrung und Schutzbedarf abgestimmt sein.
- Die aktuelle Übergabe erfolgt per E-Mail. Lieferungen mit VINs oder anderen sensiblen
  Inhalten MÜSSEN als verschlüsselter Anhang gesendet werden; das Passwort wird auf einem
  getrennten Kanal übermittelt.
- tapinomahub ordnet jedem Lieferanten genau eine feste `partner_id` zu und prüft diese vor
  dem internen Import.
- VINs werden ausschließlich intern und zugriffsgeschützt verarbeitet.
- Für analytische Ausgaben SOLL statt der vollständigen VIN eine pseudonyme Fahrzeug-ID
  verwendet werden.
- Halternamen, Kennzeichen, Dokumentnummern und Fahrzeughalter-Adressen werden nicht
  übertragen.
- Käuferdaten werden auf Segment und Land beschränkt.
- Bild-URLs dürfen keine personenbezogenen Informationen oder frei zugängliche interne
  Zugangsdaten enthalten.
- Aufbewahrung, Rollen, Löschung und die datenschutzrechtliche Grundlage sind zwischen den
  beteiligten Parteien gesondert festzulegen.

## 18. Mindestprofil für die erste Integration

Eine Quelle ist für belastbare OE- und Fahrzeugmarktanalysen geeignet, wenn mindestens
folgende Bedingungen erfüllt sind:

- Jede physische Teileinstanz besitzt eine stabile `source_part_id`.
- Jedes Teil ist mit einer gültigen VIN beziehungsweise `source_vehicle_id` verknüpft.
- Mindestens eine primäre OE-Nummer ist vorhanden.
- Zustand, Kategorie und Einbauposition werden soweit fachlich relevant geliefert.
- Einstellzeitpunkt, initialer Preis, letzter Preis und Status des Listings sind vorhanden.
- Preis- und Statusänderungen werden ab Integrationsbeginn als Ereignisse geliefert.
- Verkäufe enthalten Datum, tatsächlichen Teilepreis, Menge, Währung und Kanal.
- Stornos, Retouren und Erstattungen werden nachgeführt.
- Auch aktive und nicht verkaufte Teile werden übertragen.
- Die Quelle erklärt ihre fachliche Vollständigkeit in `export.coverage`.

Nur Verkaufsdatensätze ohne aktiven beziehungsweise unverkauften Bestand reichen nicht aus,
um Verkaufswahrscheinlichkeit, Standzeit oder Fahrzeug-Verwertungspotenzial verlässlich zu
berechnen.

## 19. Abnahmecheck für Datenlieferanten

Vor der ersten Lieferung sollte der Datenlieferant bestätigen:

- [ ] Wir vergeben dauerhafte IDs für Fahrzeuge, physische Teile, Listings und Verkäufe.
- [ ] Mehrfachlistings desselben Teils referenzieren dieselbe Teile-ID.
- [ ] OE-Rohwerte bleiben unverändert erhalten.
- [ ] Zeitpunkte enthalten eine Zeitzone.
- [ ] Geldbeträge werden in Minor Units und mit Währung übertragen.
- [ ] Brutto-/Netto-Basis, Versand und tatsächlicher Teilepreis sind getrennt.
- [ ] Entfernte Listings werden nicht pauschal als verkauft gemeldet.
- [ ] Verkäufe, Stornos, Retouren und Erstattungen bleiben historisch erhalten.
- [ ] Snapshot beziehungsweise Delta und der enthaltene Scope sind eindeutig.
- [ ] Erneute Lieferung desselben Exports erzeugt keine Duplikate.
- [ ] Wir übertragen keine Halter- oder personenbezogenen Käuferdaten.
- [ ] Die Beispieldatei validiert gegen das mitgelieferte JSON Schema.

## 20. Versionsregeln

- Zusätzliche optionale Felder erfordern mindestens eine neue Minor-Version.
- Neue Pflichtfelder, geänderte Semantik oder entfernte Enum-Werte erfordern eine neue
  Major-Version.
- Ein Lieferant MUSS `schema_version` exakt angeben.
- Bestehende Versionen werden während einer vereinbarten Übergangsfrist parallel
  unterstützt.
