# API-Legacy-Parität — Arbeitsdokument (Stand 10.07.2026) > **Ziel (User-Vorgabe 10.07.):** Beim Cutover muss die API-Schnittstelle für > Bestandskunden **erst einmal genauso funktionieren wie vorher**. Dieses > Dokument hält den belegten Alt-Vertrag fest, listet die Paritäts-Lücken und > führt die einzelnen Schritte als Checkliste. Testdaten-Wünsche des Users > folgen (Abschnitt „Testdaten", unten). **Quellenlage:** Der komplette Alt-Code liegt im Repo unter `_businessportal24.com/` (Symfony 1.4). Analysiert am 10.07.: `apps/frontend/config/routing.yml` (service_-Routen), `plugins/PressePortalPlugin/modules/pressrelease/lib/pluginPressReleaseActions.class.php`, `modules/pressreleaseimage/actions/actions.class.php`, `modules/company/actions/actions.class.php`, `lib/ApiActions.class.php`, `lib/filters/ApiKeyGuardAuthFilter|SecureApiAccessFilter|ApiAccessFilter`, `modules/tnHttpError/templates/*.json.php`, JSON-Templates der Module. --- ## 1. Belegter Alt-Vertrag (aus dem Symfony-Code) ### Authentifizierung & Zugriff - `X-ApiKey`-Header (Filter `ApiKeyGuardAuthFilter` liest `sfGuardUserProfile.api_key`); Web-Session konnte API-Routen ebenfalls nutzen. - `GET/HEAD` verlangt Credential `apiReadAccess`, `POST/PUT/DELETE` verlangt `apiWriteAccess`, andere Methoden → 405 (`SecureApiAccessFilter`). - `X-ApiKey` auf Nicht-API-Routen → 501 (`ApiAccessFilter`). - CSRF für API-Requests deaktiviert. ### Endpoints (12 API-Routen laut routing.yml, alle mit `.json`-Suffix!) | Alt-URL (Methode) | Verhalten | |---|---| | `POST /service/pressreleases.json` | PM anlegen. Params **verschachtelt**: `press_release[company_id]` (Pflicht), `press_release[category_id]`, `press_release[language]`, `press_release[title]`, `press_release[text]`, `press_release[contacts_list][]`, ggf. `press_release[backlink_url]`. **Status-Logik:** Standard `new` (= Redaktions-Queue); hatte die Firma `hasActivePerPresskitPayment()` oder der User die Permission `immediatePublication` → **sofort `published`**. Response: `201` + `Location`-Header auf die Show-URL, Body `{}`. Es gab **keinen separaten Submit-Call** — anlegen = einreichen. | | `GET /service/pressreleases.json` | **Öffentlicher Index**: nur `status=published`, portalweit (NICHT auf den User beschränkt!), Filter `query[languages][]`, `query[categories][]`, `query[offset]`, `query[limit]`, sortiert `created_at DESC`. Response: **JSON-Array von Show-URLs** (Strings), keine Objekte. | | `GET /service/pressreleases/{id}.json` | Nur `published` (sonst 404; inaktive Firma → 404). Zählt Hits (session-dedupliziert). Response: flaches Objekt `{id, title, text, teaser_end, language, category_id, view_url, company: {viewUrl, name}}`. | | `GET /service/pressreleases/{id}/images.json` | Nur bei `published` PM. Response: **Array von Bild-Show-URLs**. | | `POST /service/pressreleases/{id}/images.json` | Nur Eigentümer (sonst 403 „You are not the creator …"). Multipart, Felder verschachtelt `press_release_image[image]` (+ title/description/copyright). Response: `201` + `Location`, Body `{}`. **Seit Bildkennzeichnung E-B4 (22.09.2026) mit `IMAGE_RIGHTS_V2=true` abgeschaltet: `410` mit Hinweis auf v1** — der Alt-Vertrag kennt weder Bildnachweis noch Rechtebestätigung und wurde nie genutzt. | | `GET /service/pressreleases/{id}/images/{imgId}.json` | Response: `{id, title, description, copyright, image_file_url}`. | | `GET /service/users/me/companies.json` | Eigene Firmen; `limit`/`offset` direkt ODER `query[limit]`/`query[offset]`. Response: **Array von Show-URLs**. | | `POST /service/users/me/companies.json` | Firma anlegen (`201` + `Location`, Body `{}`). | | `GET /service/users/me/companies/{id}.json` | Nur eigene (sonst 403). | | `GET/POST /service/users/me/companies/{id}/contacts.json` + `GET …/contacts/{id}.json` | Pressekontakte lesen/anlegen. | ### Fehlerformat - 422: `{ "global_errors": [...], "field_errors": {...} }` - andere Fehler: `{ "global_errors": [...] }` --- ## 2. Ist-Stand der neuen Aliasse (routes/legacy-api.php) **Schon da:** Key-Import (`legacy:import-api-keys`, 406 Tokens), Auth-Übersetzung X-ApiKey/api_key → Bearer, unbekannte Keys 410 + Hinweis, Rate-Limit, Eligibility-Checks, Alias-Routen für pressreleases (GET/POST), pressreleases/{id} (GET), images (GET/POST), users/me/companies (GET, Show), Rest → 410-Fallback. **Aber:** Die Aliasse zeigen direkt auf die v1-Controller — Request- und Response-Verträge sind v1, nicht Alt. Für „funktioniert genauso wie vorher" fehlt eine Kompatibilitätsschicht. ## 3. Paritäts-Lücken (Befund 10.07.) | # | Lücke | Wirkung auf Alt-Clients | |---|---|---| | L1 | **`.json`-Suffix**: Alt-URLs enden auf `.json`, unsere Aliasse matchen nur ohne Suffix | Calls wie `/service/pressreleases.json` landen im 410-Fallback → **Totalausfall** | | L2 | **Verschachtelte Params**: `press_release[...]` / `press_release_image[...]` / `query[...]` vs. flache v1-Felder | POST/Filter validieren nicht → 422 → **Anlegen unmöglich** | | L3 | **Kein Auto-Submit**: alt = anlegen ist einreichen (`new` → Redaktion bzw. sofort `published`); neu = Draft, Submit-Endpoint existiert im /service-Raum gar nicht | PMs bleiben unsichtbar als Entwürfe liegen → **Kernfunktion kaputt** | | L4 | **Index-Semantik**: alt = öffentliche published-Liste (portalweit) als URL-Array; neu = eigene PMs als Objekt-Collection | Clients, die den Index parsen/crawlen, brechen | | L5 | **Show-Shape**: alt = flaches Objekt mit `view_url`, `company.viewUrl`; neu = v1-Resource (andere Felder/Nesting), owner-scoped statt published-only | Parser brechen; nicht-eigene published PMs sind nicht mehr abrufbar | | L6 | **Create-Response**: alt = `201` + `Location` + `{}`; neu = `201` + volle Resource | vermutlich unkritisch (Location fehlt aber) | | L7 | **Fehlerformat**: alt = `global_errors`/`field_errors`; neu = Laravel `message`/`errors` | Fehlerbehandlung der Clients bricht (weich) | | L8 | **Nicht abgebildete Endpoints**: Company-CREATE, Presscontact-Index/Create/Show, Einzelbild-GET → heute 410 (bewusste Entscheidung 06.07.) | Clients, die Firmen/Kontakte per API anlegen, brechen | | L9 | KI-Deklarations-Default für /service ist umgesetzt (none/original, 10.07.) — bei Auto-Submit (L3) mitdenken | erledigt, nur verdrahten | **Einordnung:** Der bisherige „end-to-end verifiziert (200)"-Test vom 06.07. prüfte Auth + Statuscode der suffix-losen Pfade, nicht den Alt-Vertrag. Verhaltensparität ist mit dem Alt-Code jetzt exakt herstellbar. --- ## 3a. Log-Auswertung Prod (User hat Logs geliefert → `dev/api/`, 10.07.) Strukturierte Aktivitäts-Logs beider Portale (Mai–Juli 2026, `b24_activity` + `pe_activity`; 8.622 erfolgreiche API-Auths, 619 Publikationen). Ergebnis: - **Genau EIN echter Client: connektar** (`connekt.connektar.de`, User `info@connektar.de`, Legacy-ID 11768 auf bp24). Daneben nur eine tote Integration mit ungültigem Key (login_failed, kein User-Agent). - **Tatsächlich genutzte Endpoints (alle OHNE `.json`-Suffix!):** | Calls | Endpoint | Status heute | |---|---|---| | 5.935 | `POST /service/users/me/companies` | **410 — fehlt!** | | 976 | `POST /service/users/me/companies/{id}/contacts` | **410 — fehlt!** | | 594 | `POST /service/pressreleases` | Alias vorhanden, Params/Verhalten abweichend (L2/L3) | | 594 | `GET /service/pressreleases/{id}` | Alias vorhanden, Response/Sichtbarkeit abweichend (L5) | | 523 | `POST /service/pressreleases/{id}/images` | Alias vorhanden, Params abweichend (L2) | - **`pressrelease_published` feuert direkt beim POST** — connektars PMs gehen **sofort live** (Sofort-Publish-Privileg bestätigt; Flow: Firma anlegen → Kontakte → PM (sofort published) → Bilder → GET zur Kontrolle). - **Damit entschärft:** L1 (`.json`-Suffix wird real nicht genutzt), L4 (kein Index-Call). **Verschärft:** L8 — Company-/Kontakt-Create sind die meistgenutzten Calls überhaupt und heute tot. ## 3b. Entscheidungen des Users (10.07.) 1. **Zwei APIs:** Die Legacy-API (`/service/…`) bildet für Bestandskunden das Alt-Verhalten ab; die v1-API bleibt die neue, saubere Schnittstelle mit den neuen Funktionen (KI-Deklarationsfelder etc.). 2. **Sofort-Publish nachbilden + Admin-Schalter:** Standard = eingehende Legacy-PMs werden wie im Altsystem sofort veröffentlicht. Über einen **Schalter im Admin** kann stattdessen die KI-Prüfung aktiviert werden: dann Status review → KI routet (Gelb/Grün sofort veröffentlichen, Rot nicht). 3. **Rückmeldung bei Ablehnung:** War im Altsystem nicht vorhanden (connektar pollt GET — 404 bis published). Ablehnungs-Mail an den Einlieferer existiert im neuen System bereits (`notifyAuthor`); ein maschinenlesbarer Rückkanal (Status-Feld/Webhook) kommt **später als Feature der neuen API** — Integration hier nur vormerken, nicht Teil der Parität. --- ## 4. Schritte (Checkliste — hier abhaken + dokumentieren) Scope nach Log-Auswertung (§3a): **die 5 connektar-Endpoints**, verhaltensgleich. - [x] **S1 — Legacy-Compat-Controller `/service/…`** (ersetzt die direkten v1-Aliasse): - [x] `POST /service/users/me/companies` — Felder `company[name|address|country_id|phone|fax|email|website]` (+ `logo`-Datei); legacy `country_id` → neuer `country_code`; Response `201` + `Location` + `{}`; Alt-Fehlerformat. - [x] `POST /service/users/me/companies/{id}/contacts` — Felder `contact[salutation_id|title|first_name|last_name|responsibility|phone|fax|email]`; Zugriff nur eigene Firma (403-Wortlaut s. §1); Response wie oben. - [x] `POST /service/pressreleases` — Felder `press_release[company_id|contacts_list[]|category_id|language|title|text|backlink_url|…]`; Statusverhalten s. S2; KI-Deklarations-Default none (erledigt 10.07.); Response `201` + `Location` + `{}`. - [x] `POST /service/pressreleases/{id}/images` — Multipart `press_release_image[image|title|description|copyright]`; Eigentümer-Check (403 „You are not the creator of this press release"); ai_declaration-Default original (erledigt 10.07.); Response `201` + `Location` + `{}`. - **22.09.2026:** hinter `IMAGE_RIGHTS_V2` abgeschaltet (410, Bildkennzeichnung E-B4). Der Default „original“ war eine erfundene Erklärung (Decision-Update S5). - [x] `GET /service/pressreleases/{id}` — nur `published` (sonst 404 — connektars Polling-Kontrolle!), Hit-Zählung, flaches Alt-Objekt `{id,title,text,teaser_end,language,category_id,view_url,company:{viewUrl,name}}`. - [x] Alt-Fehlerformat überall: `{"global_errors":[…],"field_errors":{…}}` (422) bzw. `{"global_errors":[…]}`. - [x] Pest-Paritäts-Tests (`LegacyServiceParityTest`) — je Endpoint gegen den §1-Vertrag (inkl. connektar-Flow end-to-end: Firma → Kontakt → PM → Bild → GET). - Nicht nachgebaut (real ungenutzt, bleiben 410): GET-Index-Routen, Einzelbild-GET, Company-GET/Show, Kontakt-GET, `.json`-Suffix-Formen. GET users/me/companies + Show bleiben als vorhandene Aliasse bestehen (real nur von der toten Integration gecallt, schadet nicht). - [x] **S2 — Statusverhalten create (Entscheidung 10.07., §3b):** Default **Sofort-Publish** (Alt-Verhalten, per Log belegt); **Admin-Schalter** „KI-Prüfung für Legacy-API" → Status review + KI-Routing (Gelb/Grün auto-publish, Rot Ablehnung + Mail). Umsetzung: Setting (config + Admin-UI-Toggle), Legacy-Create wertet es aus. Blacklist/Slot-Verbrauch laufen in beiden Wegen über `PressReleaseService::publish()`. - [x] **S3 — Endpoint-Scope entschieden (10.07.):** per Log-Auswertung (§3a) — nur die 5 echten connektar-Endpoints nachbauen, Rest bleibt 410. (User hat Logs in `dev/api/` geliefert; ersetzt das frühere [EXTERN] Log-Beschaffen.) - [x] **S4 — Testdaten-Command gebaut (10.07.):** `php artisan legacy:seed-api-testdata [--portal=…] [--email=…]` — legt den API-Testkunden nach connektar-Muster an (aktiv, grandfathered, Legacy-Key via Importer, Sofort-Publish-Permission, Firma + Pressekontakt) und gibt Key + curl-Sammlung des kompletten Flows aus; idempotent (wiederholter Lauf erneuert nur den Key), gesperrt in Produktion. **Live verifiziert 10.07.:** kompletter Flow per curl gegen die lokale App (Firma 201 → Kontakt 201 → PM 201 + sofort published → GET 200 mit Alt-Shape). Weitere Testdaten-Wünsche des Users folgen (Abschnitt 5). - [ ] **S5 — Paritäts-Smoke auf Staging:** curl-Sammlung des connektar-Flows (X-ApiKey, verschachtelte Params) gegen Staging; Vergleich gegen Alt-System-Responses. - [ ] **S6 — Rückkanal Ablehnung (nachgelagert, neue API):** Status-Feld/Webhook in v1 für „abgelehnt" — Design offen; bis dahin gilt Alt-Verhalten (GET pollt, 404 = nicht veröffentlicht) + Ablehnungs-Mail. - [ ] **S7 — R6.4-Mailing ergänzen:** Legacy-Keys laufen weiter, Verhalten unverändert (Sofort-Publish bzw. KI-Schalter), KI-Deklarations-Default (Erklärungswirkung), Hinweis auf neue v1-API. ## 5. Testdaten (Platzhalter — Vorgaben vom User folgen) > User (10.07.): „hier hätte ich gerne auch selber ein paar Testdaten — dazu > gleich mehr". Sobald die Wünsche da sind, hier dokumentieren (welche Kunden, > welche PMs/Firmen, welcher Kanal: /service-Alt-Calls und/oder v1) und in S4 > umsetzen. ## 6. Protokoll - **10.07.2026:** Dokument angelegt; Alt-Vertrag vollständig aus dem Symfony-Code extrahiert (§1); Lücken L1–L9 identifiziert (§3). Vorarbeiten heute bereits erledigt: KI-Deklarations-Defaults für /service (L9), Dev-Queue-Fix (KI-Pipeline lokal lauffähig). - **10.07.2026 (2):** User liefert Prod-Aktivitäts-Logs (`dev/api/`) → Auswertung §3a: ein Client (connektar), 5 Endpoints, Sofort-Publish belegt; zwei der fünf laufen heute auf 410. User-Entscheidungen §3b festgehalten (Sofort-Publish + Admin-Schalter KI-Prüfung, zwei getrennte APIs, Rückkanal später). Schrittplan S1–S7 auf den echten Scope zugeschnitten; S3 damit entschieden. **Nächster Schritt: S1 + S2 implementieren.** - **10.07.2026 (3): S1 + S2 UMGESETZT** — eigene Legacy-Controller (`app/Http/Controllers/Api/Legacy/`) für die 5 connektar-Endpoints, verhaltensgleich (verschachtelte Params, 201+Location+{}, Alt-Fehlerformat, published-only-GET mit Hit-Zählung, Legacy-Land-Mapping). Anlegen = Einreichen; Sofort-Publish über neue Permission `api:immediate-publication` (Seeder; **connektar-User beim Go-Live zuweisen, WS-7!**) — übersteuerbar durch den neuen Admin-Schalter „KI-Prüfung Legacy-API" (`app_settings`, Toggle auf /admin/press-releases). Dazu: KI-Spalte + manueller Prüf-Button (ohne Routing) in der Admin-PM-Liste; Benachrichtigungs-Mails bei API-Einlieferern an Pressekontakt, sonst Firmen-Mail, Fallback Einlieferer (Entscheidung 10.07.). Tests: `LegacyServiceParityTest` (5, inkl. connektar-Flow end-to-end) + Bestands-Tests nachgezogen. - **Offen:** S4 Testdaten (User-Vorgaben ausstehend) · S5 Staging-Smoke · S6 Rückkanal (v1, nachgelagert) · S7 Mailing · WS-7 Permission connektar.