# Umsetzungsplan: Set-Instanzen in der AB trennen (stornierte Set-Instanz leakt)

Stand: 2026-07-03 · **Status: UMGESETZT in Branch `edi-iwofurn-fixes`** (SQL-FLOOR-Bedingungen, PHP-Instanz-Keys, Vorab-AB-Status-Filter; Regressionstest A6PLSO014043 OK: nur Instanz 2 kommt durch)
Anlass: **A6PLSO014043** — AB enthält eine in D365 stornierte Position (Meldung Fachbereich).

## 1. Diagnose (DB-verifiziert)

Der Auftrag enthält dasselbe Set `552L1A7` ZWEIMAL:
- **Instanz 1** (LINENUMBER 1.x): Kopf + 2 Komponenten, **komplett storniert** (alle Status 4).
- **Instanz 2** (LINENUMBER 2.x): Kopf + 3 Komponenten, aktiv & bestätigt.

Die Item-Query der AB (`getAllOrderItemsByOrdernumber`, `OrderResponse.php:928-996`) erkennt
Set-Zugehörigkeit nur über den `SETPOS_XLNBEGA`-Präfix (`552L1A7` / `552L1A7.xxx`) — bei zwei
Instanzen identisch. Folgen:

1. Die Kopf-Bedingung ("Set vollständig bestätigt UND mind. eine nicht stornierte Komponente")
   prüft über `SETPOS LIKE 'setId.%'` ALLE Komponenten beider Instanzen → der **stornierte
   Kopf der Instanz 1 erbt die aktiven Komponenten der Instanz 2** und rutscht in die AB.
2. Die PHP-Aggregation (`$setSubAgg`/`$setHeadExternal`, Key = `bomId`, ab
   `OrderResponse.php:579`) vermischt die Instanzen ebenfalls → BEIDE Köpfe bekommen die volle
   Komponentensumme der aktiven Instanz → **Set doppelt mit vollem Betrag, Auftragssumme überhöht**.

Referenz-Implementierung existiert bereits: `getPartiallyCancelledSets`
(`OrderResponses/Model.php:515`) gruppiert korrekt je Instanz über `FLOOR(LINENUMBER)`
("damit mehrfach bestellte Sets nicht vermischt werden") — genau diese Trennung fehlt in
Item-Query und Aggregation.

Hintergrund-Bestätigung Fachbereich (2026-07-03): Set-Köpfe werden bei Auftragserzeugung
IMMER automatisch storniert (Status 4 ist beim Kopf kein Storno-Signal); ob ein Set storniert
ist, entscheiden allein die Komponenten. Teilstornos (einzelne Komponente versehentlich
storniert) laufen bereits heute auf `edi_error` ins EDI-Cockpit und blockieren die AB —
dieser Teil bleibt unverändert.

## 2. Fix-Design

Set-Instanz = `FLOOR(LINENUMBER)` überall mitführen:

### 2.1 SQL — `getAllOrderItemsByOrdernumber` (`OrderResponse.php:928`)

Alle drei `EXISTS`/`NOT EXISTS`-Unterabfragen (Komponenten-Vollständigkeit,
Kopf-Vollständigkeit, Kopf-hat-aktive-Komponente) zusätzlich einschränken auf:

```sql
AND FLOOR(c.LINENUMBER) = FLOOR(orderLine.LINENUMBER)
```

Damit fällt der Kopf der vollstornierten Instanz raus (seine Instanz hat keine aktive
Komponente), und ein Set gilt nur als "vollständig bestätigt", wenn SEINE Instanz es ist.

### 2.2 PHP — Aggregation & Maps (`OrderResponse.php:579-609, 622-655, 713-717`)

Key von `bomId` auf **`bomId . '#' . floor(LINENUMBER)`** erweitern in:
- `$setSubAgg` (Summen/Termine je Set) — Kopf liest mit seinem eigenen Instanz-Key.
- `$setHeadExternal` (externe Nummer des Kopfs, die Subs erben).
- Set-Anzahl-Ermittlung (`getSetCount`) bekommt nur die Subs der eigenen Instanz.
- Gleiches Muster in der geplanten Positionsnummern-Logik (Kopf-Map je Instanz,
  siehe `umsetzung-positionsnummern-iwofurn.md` 5.1) direkt mit berücksichtigen.

### 2.3 Vorab-AB — Status-Filter ergänzen (`getAllPREOrderItemsByOrdernumber`, `OrderResponse.php:1016`)

Die Vorab-AB-Query hat heute GAR KEINEN Status-Filter — stornierte Zeilen gehen dort auch im
Normalfall mit. Ergänzen: normale Zeilen und Set-Komponenten nur mit
`SALESORDERLINESTATUS != 4`; Set-Köpfe analog 2.1 (Instanz hat mind. eine aktive Komponente).

## 3. Tests

1. **A6PLSO014043** (Regressionsfall): AB enthält das Set genau EINMAL (Instanz 2), Summe =
   344,27 statt doppelt; keine stornierte Position.
2. Auftrag mit einem Set, vollstorniert → Set fehlt komplett in der AB (wie bisher gewollt).
3. Auftrag mit einem aktiven Set (eine Instanz) → unverändert.
4. Teilstorniertes Set → weiterhin `edi_error` + AB blockiert (Verhalten unverändert).
5. Zwei AKTIVE Instanzen desselben Sets → beide erscheinen, jede mit den Summen/Terminen
   IHRER Komponenten.
6. Vorab-AB mit stornierter Einzelposition → Position fehlt.

## 4. Offene Punkte

- [ ] GO einholen.
- [ ] Randfall klären: verlässt sich die Instanz-Gruppierung auf "Ganzzahlteil der LINENUMBER
      = Instanz"? (In allen geprüften Beispielen so; gleiche Annahme wie
      `getPartiallyCancelledSets`.)
- [ ] Nicht Teil dieses Schnitts (bewusst verworfen 2026-07-03): separater Monitoring-Batch
      für Teilstornos außerhalb des AB-Laufs; aktive Storno-Kommunikation an Kunden
      (ORDCHG/`CanceledQuantity`).
