# Umsetzungsvorschlag: Eindeutige Positionsnummern + Kundenpositions-Referenz (iwofurn)

Stand: 2026-07-06 · **Status: UMGESETZT in Branch `edi-iwofurn-fixes`** (AB + Invoice + DESADV; offen: `OrderResponseLineRef` in der Invoice) · Betrifft: OrderResponse (ORDRSP), InvoiceResponse (INVOICE), DesadvResponse (DESADV)

> **FINALE NUMMERIERUNG (Entscheidung 2026-07-04, Lobster-Mapping für beide Felder aktiv):**
> Belegposition (`Position`/`OrderPosition` → `LineItemNumber`) = **fortlaufend 1..n je AB**,
> von oben nach unten (Zeilen nach D365-LINENUMBER sortiert), unabhängig vom Zeilentyp —
> per Konstruktion eindeutig. Set-Hierarchie via `ParentPosition`. Die Kundenposition
> (`EXTERNALLINENUM_XLNBEGA`) geht UNVERÄNDERT (wie vom Kunden erhalten) ausschließlich als
> Referenz in `OrderLineRef.DocRefLineNumber` (Set-Teile erben die des Kopfs).
> Damit ersetzt: Option-B-Kompromiss und die Zwischenvariante "D365-Ganzzahl".

## 1. Ausgangslage / Problem

Wir übergeben pro Position nur **ein** Feld `OrderPosition` an Lobster. Darin vermischen sich
zwei Konzepte:

| Nachricht | Quelle von `OrderPosition` heute |
|---|---|
| OrderResponse | `EXTERNALLINENUM_XLNBEGA` ("alle oder keine") → `CUSTOMERSLINENUMBER` → `intval(LINENUMBER)` (`OrderResponse.php:711-731`) |
| Invoice | faktisch immer `intval(SalesOrderLineV3Staging.LINENUMBER)` — der Kundenpositions-Zweig ist wegen Alias-Bug tot (siehe 5.2) |
| Desadv | `CustPackingSlipTransStaging_XLNBEGA.LINENUM` (D365-Nummer) |

**Kernproblem Sets:** Der Kunde bestellt ein Set als EINE Position (z.B. Pos. 10). D365 zerlegt
das Set in Kopf + Komponenten (`SETPOS_XLNBEGA`). In der AB erben die Unterpositionen bewusst die
externe Nummer des Kopfs (`OrderResponse.php:713-717`) → mehrere ITEMS mit identischem
`OrderPosition`. Wenn Lobster das auf `LINE/LineItemNumber` mappt, entsteht ein Beleg mit
doppelten Positionsnummern — **unzulässig**, `LineItemNumber` ist die eindeutige Belegpositionsnummer.
In der Rechnung entstehen Dubletten zusätzlich durch `intval("1.1") = intval("1.2") = 1`.

## 2. Verifizierte Schema-Fakten (XML Einrichten V-1.3, ORDRSP/INVOICE `LINE`)

Quelle: https://www.integrated-worlds.com/xml_einrichten_schema.de/v1.3/ (XSD-Auszug geprüft)

| Element | Kardinalität | Typ | Bedeutung |
|---|---|---|---|
| `LineItemNumber` | **Pflicht** | `xs:int`, max. 6 Stellen (**keine Dezimalwerte!**) | Eindeutige Nummer der Belegposition (unsere) |
| `ParentLineItemNumber` | optional | wie oben | Hierarchie: Unterposition zeigt auf Kopfzeile |
| `OrderLineRef` | optional | `DocumentLineReference` | Referenz auf die **Bestellposition des Kunden**; darf sich über mehrere LINEs wiederholen |
| &nbsp;&nbsp;`DocRefNumber` | Pflicht (innerhalb OrderLineRef) | DocumentNumber | Bestellnummer des Kunden |
| &nbsp;&nbsp;`DocDate` | optional | EDIDate | Bestelldatum |
| &nbsp;&nbsp;`DocRefLineNumber` | optional | LineItemNumber | Positionsnummer aus der Bestellung |
| `AdditionalLineReferences/AdditionalLineRef` | 0..∞ | DocumentLineReferenceQualified | Weitere Referenzen mit Qualifier |

Invoice-`LINE` identisch strukturiert, zusätzlich `OrderResponseLineRef` und
`DespatchAdviceLineRef` (Referenz auf AB-/Lieferschein-Position). Auf Invoice-HEAD-Ebene
existieren analog `OrderNumberRef` (senden wir bereits), `DespatchAdviceNumberRef`,
`OrderResponseRef` und `PrevInvoiceRef`.

→ **iwofurn kann beides**: eindeutige eigene Nummer + (mehrfach vorkommende) Kundenreferenz.
Das Schema ist exakt für unseren Set-Fall gebaut: n LINEs referenzieren via `OrderLineRef`
dieselbe Kundenposition.

**Intendierte Semantik (wie EDIFACT: LIN = eigene Position, RFF+LI = Bestellreferenz):**
`LineItemNumber` ist immer die eigene Positionsnummer des jeweiligen Belegs; das Matching
durch den Empfänger ist über die `...LineRef`-Elemente vorgesehen. Die Kundenposition in
`LineItemNumber` zu transportieren (unser heutiges Verhalten in der AB) ist eine
Zweckentfremdung, die nur funktioniert, solange keine Position doppelt vorkommt.

## 3. Nummerierungsstrategie

Zwei Optionen wurden abgewogen:

**Option A — LineItemNumber = laufende Nummer 1..n (schema-korrekt):** entspricht der
intendierten iwofurn-/EDIFACT-Semantik (siehe Abschnitt 2). Ändert aber die Nummern für ALLE
Aufträge (auch ohne Sets) — Empfänger, die heute (zweckentfremdet, aber praktisch) über
`LineItemNumber` matchen, brechen. Rollout-Risiko hängt komplett davon ab, ob die Partner
`OrderLineRef` auswerten → mit Lobster/iwofurn klären (Abschnitt 6.5).

**Option B — minimalinvasiv (Kompromiss):**
- **Hauptpositionen** (inkl. Set-Kopf) behalten die heutige Nummer (Kundenposition bzw. Fallback) —
  für alle Nicht-Set-Aufträge ändert sich am `LineItemNumber` NICHTS gegenüber heute.
- **Set-Unterpositionen** bekommen neue, garantiert eindeutige Nummern: fortlaufend ab
  `max(alle Hauptpositionsnummern) + 1`. (Kein `Kopf*100+i` — Kollisionsgefahr mit echten
  Kundenpositionen, und `xs:int`/6 Stellen begrenzt.)
- **Jede** Position (Kopf wie Sub) bekommt zusätzlich `OrderLineRef` mit der Kundenposition —
  Set-Subs referenzieren dort weiterhin die Kundenposition des Kopfs (heutige Erb-Logik wandert
  vom `OrderPosition`-Feld in die Referenz).
- Set-Subs bekommen `ParentLineItemNumber` = LineItemNumber des Set-Kopfs.

Damit ist das Duplikat-Problem exakt dort gelöst, wo es entsteht, ohne Bestandsverhalten zu ändern.

**iwofurn-Feedback (2026-07-03, Antwort auf unsere Anfrage) — Konzept BESTÄTIGT:**
- Set-Komponenten sollen **alle auf dieselbe `DocRefLineNumber` verweisen** (ausdrücklich
  erwünscht: "bitte verweisen Sie alle SetArtikel auf die gleiche DocRefLineNumber") —
  iwofurn konsolidiert die Unterpositionen anhand dieser Referenz zur Hauptposition,
  da die wenigsten Empfänger mit Setartikeln umgehen können.
- Für Empfänger, die die Positionsnummer mit Referenz zur Auftragsposition erwarten,
  existieren bei iwofurn bereits Lösungen — Anpassungen je Empfänger macht iwofurn,
  wir liefern nur alle nötigen Daten.
- Wichtig: **nicht groß vom Schema-Aufbau abweichen** — keine Sonderformate erfinden.

**Empfehlung differenziert nach Nachricht:**
- **Rechnung: Option A.** Die Rechnung nummeriert laut Schema-Semantik ihre Positionen selbst
  (fortlaufend 1..n); Kundenposition in `OrderLineRef`, zusätzlich `OrderResponseLineRef`
  (AB-Belegnummer aus `dynamics_order_log` + AB-Position) → geschlossene Kette
  Bestellung → AB → Rechnung. Heute sendet die Rechnung ohnehin (durch den Alias-Bug) die
  D365-Nummer, nie die Kundenposition — es gibt also kein Bestandsverhalten
  "Kundenposition in LineItemNumber", das man erhalten müsste.
- **OrderResponse: Option B als Default**, Option A sobald Lobster/iwofurn bestätigen, dass
  die relevanten Empfänger über `OrderLineRef` matchen. Hier IST das Bestandsverhalten die
  Kundenposition — ein harter Wechsel auf 1..n ohne Bestätigung wäre riskant.

## 4. Neuer JSON-Kontrakt (an Lobster)

Pro Item additiv (Bestandsfelder bleiben, bis Lobster umgestellt hat):

```json
{
  "OrderPosition": 10,              // BESTAND: bleibt vorerst wie heute befüllt (Übergangsphase)
  "Position": 10,                   // NEU: eindeutige Belegposition -> LINE/LineItemNumber
  "ParentPosition": null,           // NEU: nur bei Set-Subs -> LINE/ParentLineItemNumber
  "OrderLineRef": {                 // NEU: -> LINE/OrderLineRef
    "DocRefNumber": "4711-KDBESTELLNR",  // = HEAD/OrderNumberRef/DocRefNumber (CUSTOMERREQUISITIONNUMBER bzw. Otto: original_document_number)
    "DocDate": "2026-06-28",             // = BASEDOCUMENTDATE (wie HEAD/OrderNumberRef/DocDate)
    "DocRefLineNumber": 10               // Kundenposition (heutige OrderPosition-Ermittlung)
  },
  "Product": { "...": "unverändert" }
}
```

Beispiel Set (Kunde bestellte Pos. 10 = Set, D365: Kopf + 2 Komponenten; höchste Hauptposition im Beleg sei 30):

| Zeile | Position | ParentPosition | OrderLineRef.DocRefLineNumber |
|---|---|---|---|
| Set-Kopf | 10 | – | 10 |
| Komponente 1 | 31 | 10 | 10 |
| Komponente 2 | 32 | 10 | 10 |
| normale Pos. | 20 | – | 20 |
| normale Pos. | 30 | – | 30 |

## 5. Codeänderungen im Detail

### 5.1 `src/Api/Classes/OrderResponse.php` (ORDRSP)

1. **Vorlauf-Pass für Nummernvergabe** (vor der Items-Schleife, dort wo heute schon `$setSubAgg`/
   `$setHeadExternal` aufgebaut werden, ab `OrderResponse.php:579`):
   - Kundenposition **pro Zeile** ermitteln: `EXTERNALLINENUM_XLNBEGA`, ersatzweise
     `CUSTOMERSLINENUMBER`. Die bisherige "alle oder keine"-Regel (`orderUsesExternalLineNum`)
     **entfällt** — DB-Analyse (Prod, letzte 6 Monate): bei EDI-Aufträgen ist
     `EXTERNALLINENUM_XLNBEGA` zu 99,4% gefüllt und zu 98% identisch mit der tatsächlich
     gesendeten Kundenposition (`IncomingOrderLineStg.external_line_number`); die leeren ~0,6%
     sind i.d.R. in D365 nachgepflegte Zeilen, bei denen `CUSTOMERSLINENUMBER` ebenfalls leer
     ist. Die Alles-oder-nichts-Regel kippte also wegen EINER nachgepflegten Zeile den GESAMTEN
     Auftrag auf interne Nummern.
   - `maxMainPos` = Maximum aller Hauptpositionsnummern bestimmen; Zähler für Zeilen OHNE
     Kundenposition (Set-Subs, nachgepflegte Zeilen) bei `maxMainPos + 1` starten.
   - Map `bomId → LineItemNumber des Kopfs` füllen (für `ParentPosition`); wichtig als
     eigener Pass, weil Komponenten in der DB-Reihenfolge VOR dem Kopf kommen können.
2. **In der Items-Schleife** (`OrderResponse.php:622` ff.):
   - `$item['Position']` setzen: Zeile MIT Kundenposition = Kundenposition; Zeile OHNE
     (Set-Sub, nachgepflegt) = nächste Nummer aus dem Zähler des Vorlauf-Passes.
   - `$item['ParentPosition']` bei Set-Subs aus der Kopf-Map.
   - `$item['OrderLineRef']` NUR setzen, wenn eine Kundenposition existiert:
     `DocRefNumber` = `$docReference` (existiert schon, inkl. Otto-Sonderfall Z. 104-115),
     `DocDate` = `date("Y-m-d", strtotime($order['BASEDOCUMENTDATE']))`,
     `DocRefLineNumber` = Kundenposition. Set-Subs erben die Kundenposition des Kopfs
     (heutige Erb-Logik Z. 713-717 — jetzt legitim, weil Referenzfeld). Nachgepflegte Zeilen
     ohne Kundenposition bekommen KEIN `OrderLineRef`.
   - `OrderPosition` unverändert weiter befüllen (Übergang, s. Rollout).
   - **Kundenartikelnummer bei Set-Komponenten unterdrücken (beschlossen 2026-07-03):**
     `Product.CustomerNumber = null` bei `$isSetSub` — analog zur bestehenden Set-Logik
     (Preis 0, kein Gewicht). Heute geht sie mit raus, wenn gepflegt (DB: 4.213 von 26.122
     Sub-Zeilen der letzten 6 Monate). Auch den Lutz-Fallback (`getLutzCustomerNumber`)
     für Subs nicht mehr aufrufen.
   - **Dazu konsistent die Vorprüfung entschärfen** (`OrderResponses/Model.php`):
     Die Lutz-Pflichtprüfung (`missingLutzNumbers` bei normaler AB, `preMissingLutzNumbers`
     bei Vorab-AB) darf Set-Komponenten (SETPOS mit ".") nicht mehr einfordern — sonst
     blockieren Sets ohne gepflegte Komponenten-Nummern weiterhin die AB, obwohl die
     Nummer gar nicht mehr ausgegeben wird. Set-Kopf + normale Positionen bleiben Pflicht.
3. **Vorab-AB vs. normale AB explizit unterscheiden** (`IS_PRECONFIRMATION`):
   Es gibt zwei AB-Varianten mit unterschiedlicher Positionsbasis, die Nummernlogik muss
   beide sauber bedienen:
   - **Vorab-AB** (`getAllPREOrderItemsByOrdernumber`): ALLE Auftragspositionen (ohne
     bestätigte Termine). `ConfirmedDeliveryDate` je Position = `REQUESTEDRECEIPTDATE`.
   - **Normale AB** (`getAllOrderItemsByOrdernumber`): nur bestätigte, nicht stornierte
     Positionen; Sets atomar (nur vollständig bestätigte Sets).
   Der Vorlauf-Pass für die Nummernvergabe arbeitet auf dem jeweiligen `$orderItems`-Set und
   funktioniert für beide. ABER: Die Zeilenmengen können sich unterscheiden (Vorab = alle,
   normale AB ggf. Teilmenge) → die fortlaufenden Sub-Nummern der Vorab-AB und der späteren
   normalen AB(s) sind NICHT zwingend identisch. Das ist schema-konform (jeder Beleg
   nummeriert selbst); das Matching über Belege hinweg läuft für den Empfänger über
   `OrderLineRef` (Kundenposition, stabil) — deshalb ist die Referenz in BEIDEN Varianten
   Pflicht. Die Kopf-Map (`bomId → Kopf-Position`) und der Sub-Zähler sind je Beleg neu
   aufzubauen, nichts über Belege hinweg cachen.
   **iwofurn-Feedback zur Vorab-AB (2026-07-03) — GEKLÄRT, keine Kennzeichnung nötig:**
   Die meisten Empfänger können Vorab/Final NICHT unterscheiden — **die letzte AB ersetzt
   die vorherige**. Eine AVIS/Final-Kennzeichnung bringt daher nichts. Entscheidend ist:
   **Jede AB (auch die Vorab-AB) MUSS ein Lieferdatum enthalten** — bei der Vorab-AB das
   geforderte/geschätzte Datum. Machen wir bereits: `ConfirmedDeliveryDate` =
   `REQUESTEDRECEIPTDATE` bei `IS_PRECONFIRMATION` (`OrderResponse.php:790-793`). iwofurns
   bevorzugter Weg wäre, die AB zurückzuhalten bis ein bestätigtes Datum existiert — genau
   das tun wir für Kunden OHNE Vorab-AB-Einstellung ohnehin; die Vorab-AB ist die bewusste
   Ausnahme für Kunden, die sie wollen.

### 5.2 `src/Api/Classes/InvoiceResponse.php` (INVOICE)

Für die Rechnung gilt Option A (siehe Abschnitt 3): eigene fortlaufende Nummerierung,
Kundenposition NUR als Referenz.

1. **Bugfix Alias-Mismatch (Voraussetzung für die Referenz):**
   SQL-Alias `orderItem_CUSTOMERSLINENUM` (`InvoiceResponse.php:1385`) vs. Prüfung auf
   `orderItem_CUSTOMERSLINENUMBER` (`:1412`) → Alias in `orderItem_CUSTOMERSLINENUMBER`
   umbenennen. Die damit verfügbare Kundenposition fließt in `OrderLineRef.DocRefLineNumber`
   (NICHT mehr in `OrderPosition`/`LineItemNumber` — dort wäre sie laut Schema-Semantik falsch).
2. **Neue Felder**: `Position` = fortlaufende Rechnungsposition (1..n über die
   Items-Schleife), `OrderLineRef` (DocRefNumber = `CUSTOMERREQUISITIONNUMBER` bzw. Otto:
   `original_document_number`, DocRefLineNumber = Kundenposition aus 1.) und
   `OrderResponseLineRef` (AB-Belegnummer aus `dynamics_order_log` + AB-Position) —
   Letzteres schließt die Kette Bestellung → AB → Rechnung; setzt voraus, dass die
   AB-Position der Zeile reproduzierbar ist (gleiche Ermittlungslogik wie in 5.1).
3. **`TotalLineCharge.Reference`** (`:1114`, heute `"$nextDocumentNumber-".LINENUM`): auf die
   neue eindeutige `Position` umstellen, sonst referenzieren Set-Zeilen denselben Key.
4. **Kundenartikelnummer bei Set-Komponenten** analog zur AB unterdrücken (beschlossen
   2026-07-03, siehe 5.1), damit AB und Rechnung konsistent sind.
4. **Bekannte Baustelle (nur dokumentieren, nicht in diesem Schnitt):** der Join der
   Auftragszeile über `ITEMNUMBER` (`:1401`) dupliziert Zeilen, wenn derselbe Artikel mehrfach
   im Auftrag ist; sauber wäre Join über `INVENTTRANSID`/Zeilenreferenz, falls in
   `SalesInvoiceLineV4Staging` vorhanden. Separates Ticket.

### 5.3 `src/Api/Classes/DesadvResponse.php` (DESADV) — UMGESETZT 2026-07-06

Gleiches Muster wie AB/Rechnung, mit DESADV-Besonderheiten (Datenbefund D365, z.B.
A6PLSO011159/A6PLPS000130):

- **Der Lieferschein enthält NUR die physischen Zeilen** — Set-KÖPFE stehen nie in
  `CustPackingSlipTransStaging_XLNBEGA` (kein physischer Artikel). Deshalb: kein
  synthetischer Kopf, `ParentPosition` immer `null`; die Set-Zuordnung läuft ausschließlich
  über die gemeinsame `OrderLineRef.DocRefLineNumber` (iwofurn konsolidiert darüber).
- `OrderPosition`/`Position` = fortlaufend 1..n je DESADV (Zeilen nach `LINENUM` sortiert).
  Vorher ging das ROHE D365-`LINENUM` raus — bei Set-Komponenten DEZIMAL (`1.001`)!
- Kundenposition: Join `trans.LINENUM = SalesOrderLineV3Staging.LINENUMBER` →
  `EXTERNALLINENUM_XLNBEGA` → `CUSTOMERSLINENUMBER` → null; Set-Komponenten erben die des
  Kopfs (Kopf-Lookup per Subquery, Instanz-Trennung `FLOOR(LINENUMBER)`).
- `Product.CustomerNumber` bei Set-Komponenten unterdrückt (analog AB/Rechnung).
- `PACILINE.OrderPosition` synchron zur Item-Position.
- **Bugfix nebenbei:** die Item-Query filterte nur auf `SALESID` — bei mehreren
  Lieferscheinen je Auftrag wurden die Zeilen ALLER Lieferscheine geladen und durch den
  Jour-Join dupliziert. Jetzt Filter auf die konkrete `PACKINGSLIPID` (identifier).

## 6. Lobster: Ist-Zustand (verifiziert anhand der ExportTrees) und nötige Änderungen

Die Lobster-Input-Bäume (Exporte vom 2026-07-02, "ExportTree.csv" = Invoice,
"ExportTree (1).csv" = OrderResponse) zeigen, welche JSON-Felder Lobster überhaupt parst.
**Nicht im Baum enthaltene Felder werden kommentarlos verworfen.**

### 6.1 Fakten aus den Bäumen

- `ITEMS/OrderPosition` ist in BEIDEN Bäumen das einzige Positionsfeld, Typ **BigInteger**,
  0..1 → bestätigt: nur Ganzzahlen, mappt auf `LINE/LineItemNumber`, Duplikate laufen
  ungeprüft durch.
- **Invoice-Baum kennt bereits:** `AdditionalLineReferences/AdditionalLineRef {Qualifier, Value}`
  (nutzen wir schon für den Zertifikats-Key, `InvoiceResponse.php:1214`) sowie am HEAD
  `OrderResponse`/`OrderResponseDate`, `PackingSlip`/`PackingSlipDate`, `InternalOrder`
  (werden alle bereits befüllt, `InvoiceResponse.php:228-260`) → Referenzkette am Kopf existiert!
- **OrderResponse-Baum kennt NICHTS davon:** kein `AdditionalLineReferences`, kein
  `OrderLineRef`, kein `ParentLineItemNumber`. Jede Positions-Referenz in der AB erfordert
  zwingend eine Baum+Mapping-Erweiterung durch Lobster.
- ⚠️ Nebenbefund AB: Wir senden `ITEMS/LineCharge`, `ITEMS/LineTexts`, `HEAD/HeadTexts`,
  `HEAD/PACI(LINE)`-Äquivalente — **im ORDRSP-Baum fehlen `LineCharge`/`LineTexts`/`HeadTexts`**
  → werden vermutlich verworfen (mit Lobster klären, ob der Export vollständig/aktuell ist;
  Invoice-Baum hat `LineCharge` sehr wohl).
- ⚠️ Nebenbefund Typen: `OrderResponseQuantity.Value`, `LineVAT.VatValue`, `VatPercentage`
  sind als **BigInteger** definiert — wir senden Dezimalwerte (z.B. `round($qty, 2)`,
  Steuerbeträge). Mögliche stille Rundung/Trunkierung bei Lobster → prüfen.

### 6.2 Konsequenz: Rechnung geht (fast) ohne Lobster-Änderung

Für die **Rechnung** ist der Zielzustand ohne Baumänderung erreichbar:
1. `OrderPosition` = fortlaufende eindeutige Rechnungsposition (nur unsere Code-Änderung).
2. Kundenposition als zusätzlicher `AdditionalLineRef` mit
   `Qualifier = "OrderPosition"` (Konstante `DOCUMENTREFERENECEQUALIFIER_ORDERPOSITION`, LI)
   und `Value = <Kundenposition>` — Baum kann es; nur bestätigen lassen, dass Lobster
   den Knoten ins XML-`AdditionalLineReferences` durchreicht.
   Achtung im Code: `AdditionalLineRef` wird heute als EIN Objekt gesetzt
   (`InvoiceResponse.php:1214`) — auf Array umstellen, sonst überschreibt der
   Zertifikats-Key die Positionsreferenz (Baum erlaubt 0..99999).
3. AB-Referenz am HEAD (`OrderResponse`/`OrderResponseDate`) existiert bereits.
   `OrderResponseLineRef` auf Positionsebene bleibt Lobster-Erweiterung (Phase 3).

### 6.3 Nötige Lobster-Erweiterungen (v.a. für die AB)

1. ORDRSP-Baum + Mapping: `Position` → `LINE/LineItemNumber`, `ParentPosition` →
   `LINE/ParentLineItemNumber`, `OrderLineRef {DocRefNumber, DocDate, DocRefLineNumber}` →
   `LINE/OrderLineRef/...` (`DocRefNumber` ist im Schema Pflicht innerhalb `OrderLineRef`).
   Alternativ minimal: `AdditionalLineReferences` analog Invoice-Baum ergänzen.
2. Übergangsregel: solange `Position` fehlt (alte Einträge in `edi_orderresponse_queue`!),
   weiter `OrderPosition` auf `LineItemNumber` mappen.
3. Klären: Erwartung der Empfänger bzgl. `LineItemNumber` vs. `OrderLineRef` (Partner, die
   über die Positionsnummer matchen, müssen auf die Referenz umstellen — Standardweg).
4. Nebenbefunde aus 6.1 ansprechen (verworfene AB-Felder, BigInteger-Typen).

## 7. Rollout-Plan

| Phase | Inhalt | Lobster nötig? | Risiko |
|---|---|---|---|
| 0 | **Rechnung komplett** (5.2): Alias-Bugfix, `OrderPosition` = fortlaufend 1..n, Kundenposition als `AdditionalLineRef` (Qualifier `OrderPosition`), `AdditionalLineRef` auf Array umstellen | nur Bestätigung, dass `AdditionalLineRef` ins XML durchgereicht wird (Baum existiert, 6.2) | gering — Rechnung sendet heute ohnehin keine Kundenposition |
| 1 | **AB**: JSON additiv erweitern (5.1: `Position`, `ParentPosition`, `OrderLineRef`); `OrderPosition` bleibt wie heute | nein (Felder werden bis zur Baumerweiterung ignoriert) | keins |
| 2 | **AB**: Lobster ORDRSP-Baum + Mapping erweitern (6.3), Pilotpartner mit Set-Aufträgen testen | ja | mittel — Partner-Matching beobachten |
| 3 | DESADV nachziehen; `OrderResponseLineRef` in Invoice (Positionsebene); `OrderPosition`-Altlogik in der AB ablösen (Option A), Vorab-AB ggf. als `AVIS` kennzeichnen | ja | gering |

## 8. Testfälle (gegen UAT/Testsystem, per CLI-Zeitraum `php batch/edi_orderresponses.php "<start>" "<ende>"`)

1. Normaler Auftrag ohne Set, mit `EXTERNALLINENUM_XLNBEGA` gepflegt → `Position` == Kundenposition, `OrderLineRef` deckungsgleich.
2. Auftrag ohne externe Nummern (Fallback `CUSTOMERSLINENUMBER`/`LINENUMBER`) → keine Dubletten.
3. Auftrag mit einem Set (Kunde 1 Position) → Kopf behält Kundenposition, Subs eindeutig ab max+1, `ParentPosition` korrekt, alle `DocRefLineNumber` = Kundenposition.
4. Auftrag mit ZWEI Instanzen desselben Sets (LINENUMBER 1.x / 2.x, Gruppierung `FLOOR(LINENUMBER)`) → getrennte Sub-Nummern je Instanz.
5. Set + normale Positionen gemischt; höchste Kundenposition prüfen (Sub-Nummern kollidieren nicht).
6. Vorab-AB (`IS_PRECONFIRMATION`) mit Set.
7. Otto-Auftrag → `OrderLineRef.DocRefNumber` = `original_document_number`.
8. Rechnung zu Set-Auftrag → `Position` eindeutig, `TotalLineCharge.Reference` eindeutig, Kundenposition == AB.
9. Grenzfall: Kundenposition ≥ 999990 (xs:int, 6 Stellen) → Sub-Nummern dürfen 999999 nicht überschreiten (Guard + edi_error).
10. Set-Komponenten: `Product.CustomerNumber` leer (Kopf behält seine), AB läuft auch durch,
    wenn Komponenten KEINE Lutz-Artikelnummer gepflegt haben (Vorprüfung fordert sie nur noch
    für Kopf + normale Positionen) — Testfall z.B. AWDESO000299 (Komponenten haben dort eigene
    Nummern `0019370077xx`, die künftig nicht mehr ausgegeben werden).

## 9. Offene Punkte

- [ ] Lobster/iwofurn: Mapping-Änderung beauftragen, Übergangsregel (6.4) bestätigen.
- [ ] Fachbereich: Verhaltensänderung Rechnungs-`OrderPosition` (Bugfix 5.2.1) freigeben.
- [ ] Klären, ob Empfänger (Lutz, Otto, …) `OrderLineRef` bereits auswerten.
- [ ] Separates Ticket: Invoice-Join über `ITEMNUMBER` (5.2.4).
