# BegaHub — EDI-Middleware zwischen D365 F&O und iwofurn (via Lobster)

PHP-Anwendung (kein Framework, eigener Router/Entity-Layer) die EDI-Nachrichten zwischen
Microsoft Dynamics 365 F&O (Xalution) und dem iwofurn-Netzwerk vermittelt.
Übertragungsweg: **Wir sprechen JSON mit Lobster; Lobster übersetzt von/nach iwofurn-XML**
nach dem Schema "XML Einrichten V-1.3":
https://www.integrated-worlds.com/xml_einrichten_schema.de/v1.3/#XML%20Einrichten%20V-1-3%20Schema_xsd.html

## Datenbanken (Zugang per .env)

- **Lokale MySQL-DB** (`DB_*`, `DBHandler::getPDOConnection()`): Queues, Logs, Cockpit-Daten
  (`edi_order_queue`, `edi_orderresponse_queue`, `dynamics_order_log`, `edi_error`, `api_token`, …).
- **D365 Entitäten-DB** (`DB_*_D365`, `DBHandler::getPDOConnectionD365()`): Azure SQL mit den
  D365-Staging-Entitäten. **Read-only für Auftragsdaten** — Schema wie von Microsoft bekannt
  (`SalesOrderHeaderV4Staging`, `SalesOrderLineV3Staging`, `SalesOrderConfirmationHeaderStaging`,
  `CustCustomerV3Staging`, `EcoResReleasedProductV2Staging`, …) plus Zusatzfelder mit Suffix
  `_XLNBEGA` (z.B. `SETPOS_XLNBEGA`, `EXTERNALLINENUM_XLNBEGA`, `BARCODE_XLNBEGA`,
  `ORDERDATE_XLNBEGA`, `SALESORDERCATEGORYRECID_XLNBEGA`) und Zusatztabellen
  (`AddressDataAndGLNStaging_XLNBEGA`, `MappingLookUpStagingTable_XLNMAP`).
  Geschrieben wird dort nur in unsere eigenen Inbound-Tabellen (`IncomingOrderStg`,
  `IncomingOrderLineStg`, `IncomingOrderStgAdditionalHeader/Item`, `EDIMessageLog`).

## Nachrichtenfluss

### Eingehende Bestellungen (ORDERS, iwofurn → D365)
1. Lobster empfängt iwofurn-XML, wandelt in JSON und **POSTet an `/api/orders`**
   (`api/routes.php` → `Api\Modules\Orders\Controller::index()`).
2. `index()` legt das rohe JSON nur in der lokalen Queue `edi_order_queue` ab.
3. Cron `batch/edi_orders.php` (`action=queue` → `OrdersClass::queue()`) parst das JSON
   (Adressen, Liefertermine/-wochen, Referenzen, Dropshipping-Erkennung, Otto/Home24-Sonderfälle …)
   und schreibt in die D365-DB: `IncomingOrderStg` + `IncomingOrderLineStg` (für Xalution/D365-Import)
   sowie `IncomingOrderStgAdditionalHeader`/`...AdditionalItem` (Zusatzinfos wie
   `original_technical_sender`, `original_gln_invoice`, `original_document_number`,
   Dropshipping-Kontakte, `additional_transport_notes`, `carrier_info_*` — werden später
   für die AB wieder ausgelesen).
4. D365/Xalution importiert daraus den Verkaufsauftrag; danach existiert er in
   `SalesOrderHeaderV4Staging`/`SalesOrderLineV3Staging`. **Verknüpfung zurück zur
   Ursprungsbestellung: `IncomingOrderStg.external_document_number = SalesOrderHeaderV4Staging.CUSTOMERREQUISITIONNUMBER`.**
   `IncomingOrderStg.type`: 0 = Standard, 1 = Otto (Labels), 2 = Amazon (bekommt KEINE EDI-AB).

### Ausgehende Auftragsbestätigungen (ORDRSP / OrderResponse, D365 → iwofurn)
1. Cron `batch/edi_orderresponses.php` (nur wenn `OUTGOING_ENABLED`; Locking per Lockfile;
   optional CLI-Zeitraum `php edi_orderresponses.php "<start>" "<ende>"`, Default: letzter Tag)
   → `OrderResponses\Controller` mit `action=queue`.
2. `OrderResponses\Model::getOrderResponses()` zieht aus der D365-DB zwei Mengen:
   - **Vorab-AB** (`IS_PRECONFIRMATION`): Aufträge ohne bestätigte Termine
     (CONFIRMEDSHIPPING/RECEIPTDATE leer bzw. Platzhalter `1900-01-01`/`2100-11-17`),
     nur für Kunden mit Vorab-AB-Einstellung (`Rep_Customer::hasPreOrderResponse`).
     Identifier = `PRE_<Auftrag>_<Artikel…>` (Dedup!).
   - **Normale AB**: Aufträge mit `SalesOrderConfirmationHeaderStaging.CONFIRMATIONDATE >= :date`
     und bestätigten Positionsterminen. Identifier = `CONFIRMATIONDOCUMENTREFERENCE`.
   Dann viele Prüfungen (Intercompany-Blacklist, war-EDI-Auftrag via `hasStagingOrder`,
   manuelle Aufträge nur mit Cockpit-Einstellung, GLNs ermittelbar, Steuercodes, bestätigte
   Termine je Position, Lutz-Artikelnummern, keine teilstornierten Sets). Fehler → `edi_error`
   fürs EDI-Cockpit. Dedup gegen `dynamics_order_log` (order_number + identifier, status=1).
3. `Api\Classes\OrderResponse::generateJSON()` (erbt Konstanten/Helper von
   `Api\Classes\IwofurnResponse`) baut pro Auftrag das Dokument mit `HEAD` (DocumentNumber =
   `<SALESORDERNUMBER>-<lfd. Nr.>` via `Rep_DynamicsLog::getNextNumber`, DocumentType
   `OrderConfirmation`, Function `Confirm`, `TechnicalReceiver` = original TechnicalSender der
   Bestellung, sonst Kunden-GLN; NAD-Adressen: DeliverAddress(+EndCustomer bei Dropshipping),
   Buyer, InvoiceRecipient, Supplier, ggf. Carrier), `ITEMS` (Positionen inkl. Set-Logik:
   Kopf trägt Menge/Preis-Summe, Unterpositionen ohne Preis; Zuschläge als `LineCharge`;
   MwSt via `Rep_Dynamics::getTaxPercent`) und `FOOT` (`VatTotal` je Steuersatz,
   `AdditionalAmounts`). Straße+Hausnr. werden per `OpenAI::getStreet` zusammengeführt.
4. Ergebnis: **eine Zeile pro Auftrag** in `edi_orderresponse_queue` (`json`-Spalte, Key = Identifier)
   + Log-Eintrag in `dynamics_order_log`. Offene `edi_error` des Auftrags werden automatisch geschlossen.
5. **Freigabe:** Versand erst, wenn `release_date` gesetzt ist (Einzelfreigabe im EDI-Cockpit).
6. **Abholung durch Lobster (Pull!):** Lobster ruft `GET /api/orderresponses` auf
   (`Controller::index()`), bekommt `{"success": true, "orderresponses": [...]}` (max. 50,
   freigegeben + `finish_date IS NULL`), Queue-Zeilen werden mit `finish_date` abgeschlossen und
   je Dokument ein `EDIMessageLog` (Typ ORDERRESPONSE) in die Azure-DB geschrieben (D365-Historie).
   Lobster wandelt das JSON in iwofurn-XML v1.3 und stellt es zu.
   (`Api\Classes\LobsterAPI::sendJsonToLobster` existiert, wird für ABs aber NICHT genutzt.)

Gleiche Grundmuster für DESADV (`/api/desadvs`), INVOICE (`/api/invoices`), REMADV
(`POST /api/remadvs`) — Klassen unter `src/Api/Classes/` und `src/Api/Modules/`.

## Wichtige Konventionen / Stolperfallen

- Platzhalter-Daten in D365: `1900-01-01` = leer, `2100-11-17` = "unbestätigt" — immer beide filtern.
- `DATAAREAID` = Mandant (A1 = Bega Consult DE, A5 = HBZ, A8 = Stolkom PL, AK = BBK, …);
  fast jede D365-Query braucht `DATAAREAID`-Filter. GLN je Mandant via `Rep_Dynamics::getCompanyGLN`.
- Sets: `SETPOS_XLNBEGA` ohne Punkt = Set-Kopf (in D365 immer Zeilenstatus 4, ist KEIN Storno!),
  mit Punkt (`<bom>.<n>`) = Unterposition. Status 4 bei normalen Positionen = storniert.
- Externe Positionsnummern (`EXTERNALLINENUM_XLNBEGA`) gelten "alle oder keine" pro Auftrag,
  sonst Fallback `CUSTOMERSLINENUMBER` → `LINENUMBER`.
- Otto: DocRefNumber = `original_document_number` statt `CUSTOMERREQUISITIONNUMBER`; Otto-GLNs
  über `Settings::setOttoGlns()`.
- Batches laufen als CLI-Crons mit Lockfiles unter `batch/`; ausgehende Nachrichten nur bei
  `OUTGOING_ENABLED=true` (Prod).
- API-Zugriff IP-beschränkt (`API_ALLOWED_IPS`), teils Bearer-Token (`api_token`-Tabelle).
- Fehlerbehandlung: `EDIException`-Familie → `logError(...)` → `edi_error` (EDI-Cockpit);
  unerwartete Fehler → `sendSentry($e)`.
- Positionsnummern: `OrderPosition` vermischt heute Kundenposition und D365-`LINENUMBER`;
  bei Sets entstehen Duplikate. Zielbild (eindeutige `LineItemNumber` + `OrderLineRef` mit
  Kundenposition + `ParentLineItemNumber`): siehe `docs/umsetzung-positionsnummern-iwofurn.md`.
- Käufer-GLN auf der AB oft falsch (Filial- statt Käufer-GLN, betrifft v.a. Lutz): Original-GLN
  liegt in `IncomingOrderStgAdditionalHeader.original_gln_customer`, Käufer-Adresse kommt in der
  Order mit (`buyerAddress`). Umsetzungsplan (wartet auf GO): `docs/umsetzung-kaeufer-gln-ab.md`.
- Liefer-GLN oft = Rechnungs-GLN (z.B. AKPLSO008265): `getDeliveryGLN` macht fragilen
  6-Felder-Textmatch und fällt auf `getInvoiceGLN` zurück; korrekt wäre der Key-Join
  `DELIVERYADDRESSLOCATIONID → GLNPartyCodeStaging_XLNEDI`. Plan (wartet auf GO):
  `docs/umsetzung-liefer-gln-locationid.md`.
- Teuerungszuschlag fehlt auf ABs (z.B. AWDESO000399-1): Lobster-ORDRSP-Baum kennt kein
  `LineCharge` (droppt es), und der berechnete Betrag steht nicht mal im JSON (`Amount` =
  nur Fix-Anteil = 0 bei %-Zuschlägen). Sofort-Lösung über vorhandenes `AdditionalLineAmount`
  (Qualifier "Summe"/131) + FOOT-Fix: `docs/befund-teuerungszuschlag-ab.md`.
- Sets: Kopf in D365 IMMER Status 4 (kein Storno-Signal, Fachbereich bestätigt); Storno
  entscheiden die Komponenten. Teilstorno → `edi_error` + AB blockiert (gewollt). Bug: bei
  ZWEI Instanzen desselben Sets fehlt die Instanz-Trennung (`FLOOR(LINENUMBER)`) in Item-Query
  + Aggregation → stornierte Instanz leakt in die AB (A6PLSO014043). Plan (wartet auf GO):
  `docs/umsetzung-set-instanzen-ab.md`.
