API und GUI für automatisierung des Anbieterwechselprozesses
Find a file
2026-07-26 14:04:40 +02:00
.claude VueJS Beispiel Dashboard / Admin-Panel hinzugefügt 2026-07-05 08:58:19 +02:00
.claude_attachments EKP-Portal via Playwright-Scrapper auslesen, weil keine API dafür angeboten wird und in unsere Datenbank übernehmen 2026-07-26 08:40:04 +02:00
backend README aktualisiert 2026-07-26 13:36:53 +02:00
dashboard Zurück Buttons entfernt. 2026-07-26 14:04:40 +02:00
db README aktualisiert 2026-07-26 13:36:53 +02:00
ui-php Eskalationsmail implementiert. Versendung per eMail-Dienst. 2026-07-25 15:25:55 +02:00
.gitignore VueJS Beispiel Dashboard / Admin-Panel hinzugefügt 2026-07-05 08:58:19 +02:00
README.md README aktualisiert 2026-07-26 13:36:53 +02:00

Anbieterwechselauftrag / WBCI-Vorabstimmung

Software zur automatisierten Abwicklung der WBCI-Vorabstimmung beim Anbieterwechsel (Rufnummernportierung zwischen deutschen Telekommunikations- anbietern). Das System besteht aus vier unabhängigen Teilen:

  • /db MariaDB-11.4.7-Schema + Installationsanleitung
  • /backend NodeJS/Express-API: fachliche Logik, Plausibilitäts- Prüfung, PDF-Erzeugung, E-Mail-Versand, EKP-Portal-Sync
  • /dashboard PHP-Proxy + Vue-3-SPA, das produktiv genutzte Admin-Dashboard (Übersicht, Formulare, Eskalationen, EKP-Verzeichnis)
  • /ui-php PHP-5.3-kompatible Test-Oberfläche (ein Formular pro Aktion, kein SPA), spricht wie das Dashboard ausschließlich über die Klasse Anbieterwechsel\ApiClient mit der API

Die API ist die einzige Stelle mit Fachlogik. Jeder Client (Dashboard, Test-UI, curl, ein eigenes Frontend) spricht über HTTP/JSON mit ihr.

Umsetzungsstand: Der komplette Lebenszyklus einer Vorabstimmung ist umgesetzt: WBCI-GF1/GF2/GF3 (Schritt 1, aufnehmend und abgebend/ eingehend), Schritt-2-Antwort (Zustimmung/Ablehnung), AKM-TR (Schritt 3, mit Modal zur Auswahl der vom EKPabg tatsächlich bestätigten Rufnummern) inkl. ABBM-TR (Ablehnung der Mitteilung), Storno (STR-AUF/STR-AEN) und Terminverschiebung (TVS-VA), eine Eskalationsliste für überfällige Vorgänge inkl. Eskalations-E-Mail direkt aus dem Tool, sowie ein Vue-Dashboard als produktive Oberfläche. Ein eigener Sync-Job (backend/scripts/sync-ekp-portal.js) holt granulare Clearing-Kontakte, Portierungskennungen und die für VA-Anfragen maßgeblichen Mail/Fax- Kontakte aus dem EKP-Portal (cockpit.xc.en.enghousehosted.com) und ersetzt damit den bisherigen manuellen CSV-Import als primäre Quelle für die Empfängerauflösung beim E-Mail-Versand. Offen (siehe Roadmap): bilaterale 2-Schritt-Variante, automatischer Mailversand der Eskalationsliste an ein zentrales Postfach.

Schnellstart

# 1. Datenbank einrichten (siehe db/README.md fuer Details) - alle Dateien
# in db/schema/ der Reihe nach einspielen (001_... bis 014_...)
mysql -u root -p < db/schema/001_accounts_tokens.sql   # usw., siehe db/README.md

# 2. Backend
cd backend
npm install                 # installiert u.a. Playwright (fuer den EKP-Portal-Sync)
cp .env.example .env        # Zugangsdaten eintragen (DB + SMTP + EKP-Portal)
node scripts/import-ekp-csv.js /pfad/zur/firmen-liste.csv   # Basis-Import (schnell, aber nur generischer Kontakt)
node scripts/create-account.js --carrier-code=RSMBGL --country-code=DEU --provider-name="RSM Freilassing"
npm start                   # startet auf http://localhost:3000

# 2b. EKP-Portal-Sync (optional, aber empfohlen - liefert die fuer den
# E-Mail-Versand tatsaechlich massgeblichen Kontakte, siehe Abschnitt
# "EKP-Portal-Sync" unten). Dauert bei ~770 Firmen ca. 60-90 Minuten.
npx playwright install chromium
node scripts/sync-ekp-portal.js all

# 3. Dashboard (produktive Oberflaeche)
cd ../dashboard
cp config.example.php config.php
# AWA_API_BASE_URL + AWA_API_TOKEN (Bearer-Token aus Schritt 2) eintragen
php -S 127.0.0.1:8080 -t public

# 4. Test-UI (optional, einzelne Formulare statt SPA)
cd ../ui-php
# AWA_API_TOKEN in config.php mit demselben Bearer-Token befuellen
php -S 127.0.0.1:8081 -t public

Authentifizierung

Alle Endpunkte unter /v1/* erfordern einen Bearer-Token (angelegt via scripts/create-account.js, siehe db/README.md). Der Token identifiziert den Account (ITU Carrier Code, Anbietername, fortlaufender Zähler für Vorabstimmungs-IDs):

curl -H "Authorization: Bearer <TOKEN>" http://localhost:3000/v1/ekp

Fehlt der Token oder ist er ungültig, antwortet die API mit 401.


API-Referenz

Basis-URL in den Beispielen: http://localhost:3000. $TOKEN steht für den Bearer-Token aus create-account.js.

Health-Check (ohne Auth)

curl http://localhost:3000/health
# {"status":"ok"}

EKP-Liste durchsuchen (Auswahlfeld für abgebenden/aufnehmenden EKP)

GET /v1/ekp?search=<Suchbegriff> durchsucht Firmenname und ITU Carrier Code (Import aus der CSV-Firmenliste, siehe db/README.md). Ohne search werden die ersten 100 Einträge alphabetisch geliefert.

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/v1/ekp?search=Telekom"
{
  "items": [
    {
      "id": 319,
      "bnetza": "93/007",
      "itu_carrier_code": "DEU.DTAG",
      "company_name": "Telekom Deutschland GmbH",
      "email": "hilfe-anbieterwechsel@telekom.de",
      "hotline": "0800 330 9366 027"
    }
  ]
}

In der PHP-Test-UI ist die EKP-Auswahl als eigener AJAX-Baustein umgesetzt (inc/ekp_picker.php + assets/ekp-picker.js, angesprochen über den Proxy ajax_ekp.php): Tippen im Suchfeld fragt die Liste live nach, ohne die Seite neu zu laden andere bereits ausgefüllte Formularfelder bleiben dadurch erhalten. Wiederverwendbar über awa_render_ekp_picker($fieldName, $code, $label). Im Dashboard übernimmt das die Vue-Komponente EkpPicker (dashboard/public/assets/components/common.js), fachlich identisch.

EKP-Verzeichnis (vollständige Kontaktübersicht)

GET /v1/ekp/directory liefert alle Firmen mit sämtlichen aus dem EKP-Portal synchronisierten Daten auf einmal (kein search-Parameter die Freitextsuche läuft im Dashboard clientseitig über alle Felder, analog zur Dashboard-/Eskalationsliste). Pro Firma zusätzlich zu den Basisdaten aus GET /v1/ekp:

  • commercial_register_nr/commercial_register_place, street/ house_number/postcode/city/country Handelsregister/Anschrift
  • portidents von der Firma selbst verwendete Portierungskennungen (Portal-Sektion „Portident/s (PI)“)
  • foreignPortidents Portierungskennungen anderer Firmen, die diese Firma referenziert hat, je mit Beschreibung, wem der Code gehört (Portal-Sektion „Foreign Portident/s (PI)“ „Fremde Portierungskennung/en (PK)“)
  • vaContacts Kontakte aus „Contact details for provider change ... by mail or fax“ (E-Mail/Fax/Beschreibung, oft rollenspezifisch z.B. „... ist abgebender EKP“) für unsere ausgehenden VA-Anfragen als aufnehmend die eigentlich maßgebliche Quelle (siehe „EKP-Portal-Sync“ unten)
  • clearingContacts granulare Clearing-Kontakte je Thema (2.1 Vorabstimmung … 2.6 Verbraucheranfragen), je mit verknüpften Szenario-Codes aus dem AK-SPRI-Clearing-Handbuch (scenarios)
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/v1/ekp/directory

Im Dashboard: eigene Seite „EKP-Verzeichnis“ (#/ekp) mit Freitextfilter und Akkordeon-Detailansicht je Firma; Telefonnummern/Fax als tel:-Links, E-Mail-Adressen als mailto:-Links.

Neue Vorabstimmung anlegen (Schritt 1, Rolle aufnehmend)

POST /v1/vorabstimmungen

Legt eine neue Vorabstimmungsanfrage an und erzeugt dabei die Vorabstimmungs-ID. Optionales Feld wbci_gf wählt den Geschäftsfall: VA-KUE-MRN (WBCI-GF1, Kündigung mit Rufnummernportierung, Default), VA-KUE-ORN (WBCI-GF2, Kündigung ohne Rufnummernportierung) oder VA-RRNP (WBCI-GF3, reine Rufnummernportierung). Je nach wbci_gf werden "Kündigung angefragt" (F2) bzw. "Portierung beauftragt" (F4) automatisch passend gesetzt und die jeweils relevanten Pflichtfelder geprüft.

Die API prüft alle Pflichtfelder und Regellaufzeiten (Fristen). Gibt es irgendeine Meldung (Fehler, Warnung oder Information), wird nichts gespeichert die Meldungen kommen mit HTTP 422 zurück. Erst ein erneuter Aufruf mit "force": true speichert die Daten trotzdem (Status wird dann forciert statt gespeichert). Nur wenn beim ersten Versuch keinerlei Meldungen auftreten, wird sofort mit gespeichert (HTTP 201) gespeichert.

Felder (siehe Formularfeld-Referenz F1F66 in Klammern):

Feld Typ Bedeutung
wbci_gf string VA-KUE-MRN (Default) | VA-KUE-ORN | VA-RRNP (F21)
counterpart_itu_code string ITU Carrier Code des abgebenden EKP (F3), z.B. DEU.DTAG
kunde_name, kunde_vorname string Endkunde (F5/F6)
kunde_strasse, kunde_hausnummer, kunde_plz, kunde_ort string Adresse (F7F10)
alle_rufnummern_portieren bool F11 (nur relevant bei Einzelrufnummern, nicht bei Rufnummernblock)
ortsnetzkennzahl string F12, ohne führende 0
rufnummern string[] F13, einzelne Rufnummern (MSN)
durchwahl_rn, abfragestelle, block_von, block_bis string Anlagenanschluss/Rufnummernblock (F14F17)
pki_auf string PKIauf (F24) Pflicht bei VA-KUE-MRN/VA-RRNP, entfällt bei VA-KUE-ORN
wechseltermin date (YYYY-MM-DD) F25
fenster_0608, fenster_0612, fenster_sonstiges bool Portierungsfenster (F27F29)
fenster_sonstiges_text string F30, Pflicht wenn fenster_sonstiges=true
rueckinfo_ansprechpartner, rueckinfo_fax_email, rueckinfo_tel string F31F33
ressourcenuebernahme "ja" | "nein" F34/F35
sicherer_hafen bool F36
interne_bemerkung string F66
force bool erneutes Senden trotz vorhandener Meldungen

Bei VA-KUE-MRN/VA-RRNP (Portierung findet statt) muss entweder rufnummern (einzelne MSN) oder block_von+block_bis (Anlagenanschluss/Rufnummernblock) angegeben werden nie beides gleichzeitig. durchwahl_rn/abfragestelle sind ergänzende Angaben zum Rufnummernblock.

Beispiel unvollständiger Antrag (wird abgelehnt, nichts gespeichert):

curl -s -X POST http://localhost:3000/v1/vorabstimmungen \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "counterpart_itu_code": "DEU.DTAG",
    "kunde_name": "Kern",
    "kunde_vorname": "Alois",
    "ortsnetzkennzahl": "8654",
    "rufnummern": ["5769745"],
    "wechseltermin": "2026-07-05"
  }'
// HTTP 422
{
  "saved": false,
  "errors": [
    { "field": "F7", "code": "kunde_strasse_fehlt", "message": "Strasse fehlt." },
    { "field": "F24", "code": "pki_auf_fehlt", "message": "Portierungskennung aufnehmend (PKIauf, F24) fehlt." },
    { "field": "F34-F35", "code": "ressourcenuebernahme_fehlt", "message": "Angabe zur Ressourcenuebernahme (ja/nein) fehlt." }
    // ...
  ],
  "warnings": [
    { "field": "F27-F30", "code": "portierungsfenster_fehlt", "message": "Kein Portierungsfenster ausgewaehlt (06-08, 06-12 oder sonstiges)." }
  ],
  "infos": [
    { "field": "F25", "code": "frist_kurzfristig", "message": "Wechseltermin liegt weniger als 7 Arbeitstage in der Zukunft (Regel-Vorlaufzeit unterschritten)." }
  ]
}

Beispiel derselbe Antrag mit force: true (wird trotzdem gespeichert):

curl -s -X POST http://localhost:3000/v1/vorabstimmungen \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "counterpart_itu_code": "DEU.DTAG",
    "kunde_name": "Kern",
    "kunde_vorname": "Alois",
    "ortsnetzkennzahl": "8654",
    "rufnummern": ["5769745"],
    "wechseltermin": "2026-07-05",
    "force": true
  }'
// HTTP 201
{
  "saved": true,
  "status": "forciert",
  "processId": "fd679a95-396c-4dcc-b17f-3a1d03936bb6",
  "versionId": "aa83557e-d78c-4b5d-918c-33377d13d622",
  "vorabstimmungsId": "DEU.RSMBGL.V260703001",
  "errors": [ /* ... */ ],
  "warnings": [ /* ... */ ],
  "infos": [ /* ... */ ]
}

Beispiel vollständiger, plausibler Antrag (wird sofort ohne force gespeichert):

curl -s -X POST http://localhost:3000/v1/vorabstimmungen \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "counterpart_itu_code": "DEU.DTAG",
    "kunde_name": "Kern",
    "kunde_vorname": "Alois",
    "kunde_strasse": "Spitz",
    "kunde_hausnummer": "2",
    "kunde_plz": "83416",
    "kunde_ort": "Saaldorf-Surheim",
    "ortsnetzkennzahl": "8654",
    "rufnummern": ["5769745", "7701848", "7701849"],
    "alle_rufnummern_portieren": true,
    "pki_auf": "12345",
    "wechseltermin": "2026-08-15",
    "fenster_0612": true,
    "ressourcenuebernahme": "ja",
    "rueckinfo_ansprechpartner": "Michael Rack",
    "rueckinfo_tel": "08654 1234"
  }'
// HTTP 201
{
  "saved": true,
  "status": "gespeichert",
  "processId": "9afb7e62-3925-4ce0-a4c5-1560556959e6",
  "versionId": "bd416d6d-7d7b-4dfb-9921-ec4d9ccb911e",
  "vorabstimmungsId": "DEU.RSMBGL.V260703002",
  "errors": [], "warnings": [], "infos": []
}

Eingehende Vorabstimmung erfassen (Schritt 1, Rolle abgebend)

POST /v1/vorabstimmungen/incoming

Da es laut Spezifikation keinen automatisierten Kanal zwischen Carriern gibt (Austausch läuft über Fax/E-Mail), wird eine von der Gegenseite erhaltene Anfrage hierüber manuell nacherfasst. Anders als beim normalen Anlegen wird die Vorabstimmungs-ID mitgegeben (vorabstimmungs_id, von der Gegenseite vergeben) statt generiert. Der Name des aufnehmenden EKP wird automatisch aus dem Carrier-Code-Segment der ID über die EKP-Liste aufgelöst. Enthält die ID unseren eigenen Carrier Code oder entspricht sie nicht der Bildungsregel, wird der Vorgang abgelehnt (HTTP 422) das lässt sich nicht per force übergehen, da sonst ein Prozess mit widersprüchlicher Rolle entstünde.

curl -s -X POST http://localhost:3000/v1/vorabstimmungen/incoming \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "wbci_gf": "VA-KUE-MRN",
    "vorabstimmungs_id": "DEU.DTAG.V260701042",
    "kunde_name": "Fremd", "kunde_vorname": "Fritz",
    "kunde_strasse": "Ferne Str", "kunde_hausnummer": "9", "kunde_plz": "83395", "kunde_ort": "Freilassing",
    "ortsnetzkennzahl": "8654", "rufnummern": ["5551234"], "alle_rufnummern_portieren": true, "pki_auf": "67890",
    "wechseltermin": "2026-09-10", "fenster_0612": true, "ressourcenuebernahme": "ja"
  }'

Schritt-2-Antwort (Zustimmung/Ablehnung), ABBM-TR, Antwort auf TVS-VA bzw. auf Storno

POST /v1/vorabstimmungen/:processId/antwort

Erfasst die Antwort auf die letzte Version eines Prozesses der Kontext wird automatisch anhand von deren step erkannt:

  • letzte Version ist eine Anfrage (anfrage) → reguläre Schritt-2-Antwort (Zustimmung oder Ablehnung), Felder wie unten.
  • letzte Version ist eine AKM-TR-Mitteilung (mitteilung) → automatisch nur outcome: "ablehnung" zulässig (ABBM-TR); ein Zustimmungsversuch liefert HTTP 409 (not_applicable).
  • letzte Version ist eine Terminverschiebung (terminverschiebung, TVS-VA) → bei Zustimmung ist ausschließlich zwa vorgesehen (nat/ada sind hier ein Fehler), bei Ablehnung ausschließlich son mit Pflicht-grund_text (die übrigen Ablehnungsgründe sind hier ein Fehler).
  • letzte Version ist ein Storno (storno_aufhebung/storno_aenderung, STR-AUF/STR-AEN) → outcome: "zustimmung" bedeutet „Storno ausgeführt: ja“ (F37, keine weiteren Angaben nötig), outcome: "ablehnung" bedeutet „Storno ausgeführt: nein“ (F38, dann ist grund_text Pflicht). Die Rolle des Antwortenden ist automatisch die Gegenrolle zu der Partei, die das Storno gesendet hat (Storno kann von aufnehmend oder abgebend ausgehen).
Feld Bedeutung
outcome "zustimmung" oder "ablehnung"
zwa, nat, ada Bei Zustimmung: F39F41 (bei TVS-VA nur zwa zulässig)
bestaetigter_wechseltermin, ist_technologie, wita, spri, wita_vertragsnummer Bei Zustimmung: F42F46 (nicht bei TVS-VA/Storno)
adf, kni, vae, rng, wai, aif, son Bei Ablehnung: F48F54 (bei TVS-VA nur son zulässig, bei Storno nicht relevant)
grund_text F47, Pflicht bei son (bei TVS-VA immer Pflicht bei Ablehnung, bei Storno Pflicht bei outcome: "ablehnung")
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/antwort \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outcome":"zustimmung","zwa":true,"ist_technologie":"002 TAL DSL","wita":true,"wita_vertragsnummer":"WITA-99887"}'

# Antwort auf eine Terminverschiebung (letzte Version = terminverschiebung)
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/antwort \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outcome":"zustimmung","zwa":true}'
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/antwort \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outcome":"ablehnung","son":true,"grund_text":"Termin bereits anderweitig verplant."}'

# Antwort auf ein Storno (letzte Version = storno_aufhebung/storno_aenderung)
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/antwort \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outcome":"zustimmung"}'
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/antwort \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outcome":"ablehnung","grund_text":"Stornierung wurde bereits ausgefuehrt."}'

AKM-TR (Schritt 3: Übernahme der technischen Ressource)

POST /v1/vorabstimmungen/:processId/akm-tr

Nur für WBCI-GF1/GF2 (nicht GF3, dort gibt es keinen Schritt 3 Versuch liefert HTTP 409). Rolle immer aufnehmend.

Hat der EKPabg in der Zustimmung (Schritt 2) eigene Rufnummern mit PKIabg mitgeteilt (z.B. bei „Alle Rufnummern portieren“), verlangt AKM-TR eine neue, explizite Auswahl der tatsächlich zu übernehmenden Rufnummern über rufnummern (string[], muss eine Teilmenge der vom EKPabg mitgeteilten Nummern sein unbekannte Nummern liefern einen nicht per force übergehbaren Fehler). Der Anfragebereich der neuen Version enthält danach nur noch die ausgewählten Nummern, alle_rufnummern_portieren wird zurückgesetzt. Hat der EKPabg keine Einzelrufnummern mitgeteilt (z.B. Rufnummernblock), entfällt die Auswahl und die bisherige Anfrage wird unverändert übernommen. Im Dashboard öffnet der Button „AKM-TR erstellen“ dafür ein Modal mit den vom EKPabg mitgeteilten Rufnummern zum Abwählen.

curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/akm-tr \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"ressourcenuebernahme":"ja","sicherer_hafen":false,"rufnummern":["5769745","7701848"]}'

Storno (STR-AUF / STR-AEN)

POST /v1/vorabstimmungen/:processId/storno

typ: "aufhebung" (STR-AUF) oder "aenderung" (STR-AEN, Default aufhebung). Kann von aufnehmend oder abgebend ausgehen. Wird performed_by_role weggelassen, senden wir selbst (unsere Rolle im Prozess) und die API erzeugt automatisch eine neue Storno-ID. Soll stattdessen eine von der Gegenseite per Fax/E-Mail mitgeteilte Storno-Aktion nacherfasst werden, performed_by_role auf die sendende Rolle setzen und deren storno_aenderungs_id mitgeben (Pflicht in diesem Fall, nicht per force übergehbar).

# Wir stornieren selbst
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/storno \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"typ":"aufhebung","grund_text":"Kunde hat Auftrag storniert."}'

# Gegenseite (abgebend) hat storniert, wir erfassen es nach
curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/storno \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"typ":"aenderung","performed_by_role":"abgebend","storno_aenderungs_id":"DEU.DTAG.S260701001"}'

Terminverschiebung (TVS-VA)

POST /v1/vorabstimmungen/:processId/terminverschiebung

Nur von der Rolle aufnehmend zulässig. Sind wir selbst abgebend, muss die von der Gegenseite mitgeteilte Änderungs-ID (storno_aenderungs_id) mitgegeben werden (analog zu Storno).

curl -s -X POST http://localhost:3000/v1/vorabstimmungen/$PROCESS_ID/terminverschiebung \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"neuer_wechseltermin":"2026-09-15","grund_text":"Technischer Grund beim Kunden."}'

Eskalationsliste

GET /v1/eskalationen (JSON) bzw. GET /v1/eskalationen?format=csv (CSV- Download) listet Prozesse, deren letzter Schritt die Regel-Reaktionszeit überschritten hat (Anfrage ohne Antwort ≥10 Arbeitstage, Storno/Änderung/ Terminverschiebung ohne Reaktion ≥5 Arbeitstage) ersetzt die in der Spezifikation beschriebene manuelle Excel-Liste per zentralem Postfach. Die JSON-Antwort enthält zusätzlich process_id/version_id sowie role (die Rolle der Master-Version, also wer den Prozess ursprünglich gestellt hat) je Eintrag.

curl -s http://localhost:3000/v1/eskalationen -H "Authorization: Bearer $TOKEN"
curl -s "http://localhost:3000/v1/eskalationen?format=csv" -H "Authorization: Bearer $TOKEN" -o eskalationsliste.csv

Für Einträge mit step: "anfrage" und role: "aufnehmend" (wir warten auf die Schritt-2-Antwort des EKPabg) bietet das Dashboard direkt einen Button „Eskalation an EKP senden“ erstellt darüber denselben E-Mail-Entwurf-Flow wie unten, nur mit kind: "eskalation" (siehe nächster Abschnitt).

Wechselprozesse auflisten (Dashboard/Historie)

GET /v1/prozesse?status=offen alle noch nicht abgeschlossenen Prozesse (unpaginiert), sortiert: Prozesse, die auf eine Rückmeldung warten (Anfrage, Storno, Terminverschiebung), zuerst und darunter älteste zuerst (am dringlichsten für die Einhaltung der Regel-Antwortfristen); danach Prozesse, die nur noch auf AKM-TR warten (tier: 1).

GET /v1/prozesse?status=abgeschlossen&page=1&pageSize=30 abgeschlossene Prozesse (AKM-TR erfolgt, abgelehnt, oder bei VA-RRNP bereits mit der Zustimmung abgeschlossen) der letzten 6 Monate, neueste zuerst, seitenweise paginiert (hasMore zeigt, ob eine weitere Seite existiert).

curl -s "http://localhost:3000/v1/prozesse?status=offen" -H "Authorization: Bearer $TOKEN"
curl -s "http://localhost:3000/v1/prozesse?status=abgeschlossen&page=2&pageSize=30" -H "Authorization: Bearer $TOKEN"

In der PHP-Test-UI wird diese Liste auf index.php per AJAX über den serverseitigen Proxy ajax_prozesse.php geladen (der Browser bekommt nie den Bearer-Token zu Gesicht, ajax_prozesse.php spricht serverseitig mit der API).

E-Mail-Text automatisch verarbeiten

POST /v1/email-import verarbeitet den kompletten, maschinenlesbaren E-Mail-Body, wie ihn z.B. die Telekom zu ihren Vorabstimmungsanfragen mitschickt (Feld:Wert-Zeilen, siehe .claude_attachments/Telekom Beispiel eMail Text maschinenlesbar.txt).

  • Ist die enthaltene VorabstimmungsId unbekannt, wird eine neue eingehende Anfrage angelegt (wie POST /v1/vorabstimmungen/incoming).
  • Ist sie bekannt, wird automatisch die passende Folgeversion auf dem bestehenden Prozess erstellt: Storno (falls StornierungsId gesetzt), Terminverschiebung (falls AenderungsId gesetzt), sonst je nach Feldern und aktuellem Stand des Prozesses eine Schritt-2-Antwort (ZWA/NAT bzw. ADF/KNI/VAE/RNG/WAI/AIF/SON gesetzt) oder eine AKM-TR-Mitteilung (assumption_of_resources gesetzt, wenn die letzte Version eine Zustimmung war).

Die Antwort enthält zusätzlich action (incoming_anfrage|antwort|akm_tr|storno|terminverschiebung) und folgt ansonsten demselben Muster wie die zugrunde liegenden Endpunkte (HTTP 422 mit Meldungen ohne force, HTTP 409 bei nicht anwendbaren Aktionen wie AKM-TR auf einem VA-RRNP-Prozess).

curl -s -X POST http://localhost:3000/v1/email-import \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data-binary @- <<'EOF'
{"text": "Sie erhalten hiermit eine Vorabstimmungsanfrage ...\n\nVorabstimmung\nCarriercodeAuf:0390\n...\nVorabstimmungsId:DEU.RSMBGL.V260701001\n..."}
EOF

In der PHP-Test-UI: email_import.php (Textfeld zum Einfügen des E-Mail-Bodys).

Feld-Mapping (Auszug, siehe backend/src/services/emailImportService.js für die vollständige Liste): surname/givenname→Name/Vorname, street/number/zipcode/city→Adresse, area_code/call_numbers→Ortsnetzkennzahl/Rufnummern, WBCI_case→Geschäftsfall, date_of_churn→Wechseltermin (erkennt DD.MM.YYYY, YYYYMMDD und YYYY-MM-DD), ZWA/NAT/ADA/ADF/KNI/VAE/RNG/WAI/AIF/SON→ gleichnamige Antwort-Felder, VorabstimmungsId/StornierungsId/AenderungsId→ Prozess-Zuordnung. call_numbers_to_port→Rückmelde-Rufnummernliste; jede Zeile enthält Rufnummer und PKIabg mit "-" verbunden (z.B. 500-D001) und wird in Rufnummer/PKI aufgesplittet (siehe echtes Beispiel in .claude_attachments/bestaetigung_telekom.txt, Zeile 62ff.). Der Feldname DDI_number_to_port_left/rightantwort_durchwahl_rn/antwort_abfragestelle ist weiterhin eine plausible, aber unbestätigte Annahme, da in der bisher bekannten Beispieldatei nicht befüllt.

Vorabstimmung + Versionshistorie abrufen

GET /v1/vorabstimmungen/:processId liefert den Mutter-Prozess mit allen Versionen (jede Änderung/jeder Schritt ist eine eigene versionierte Zeile), inklusive Rufnummernlisten und protokollierter Plausibilitäts-Meldungen.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/v1/vorabstimmungen/9afb7e62-3925-4ce0-a4c5-1560556959e6
{
  "process": {
    "id": "9afb7e62-3925-4ce0-a4c5-1560556959e6",
    "vorabstimmungs_id": "DEU.RSMBGL.V260703002",
    "account_id": "9be52d6d-0d3b-4481-909b-5fdd8833e8c3",
    "created_at": "2026-07-03 12:06:07"
  },
  "ourRole": "aufnehmend",
  "versions": [
    {
      "id": "bd416d6d-7d7b-4dfb-9921-ec4d9ccb911e",
      "version_no": 1,
      "wbci_gf": "VA-KUE-MRN",
      "step": "anfrage",
      "role": "aufnehmend",
      "status": "gespeichert",
      "kunde_name": "Kern",
      // ... alle Formularfelder F1-F66 ...
      "rufnummern": [
        { "section": "anfrage", "rufnummer": "5769745", "pki": null, "sort_order": 0 }
      ],
      "validation_results": []
    }
  ]
}

Unbekannte oder fremde (anderer Account) Prozess-ID → HTTP 404.

PDF abrufen

GET /v1/vorabstimmungen/:processId/versions/:versionId/pdf rendert den Anbieterwechselauftrag für die angegebene Version live als PDF (reiner Text-/Vektor-Render, kein Formular-Hintergrundbild).

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/v1/vorabstimmungen/9afb7e62-3925-4ce0-a4c5-1560556959e6/versions/bd416d6d-7d7b-4dfb-9921-ec4d9ccb911e/pdf \
  -o anbieterwechselauftrag.pdf

E-Mail-Entwurf anlegen

POST /v1/versions/:versionId/email-draft legt einen neuen E-Mail- Entwurf an (Status draft) jeder Aufruf erzeugt einen weiteren Entwurf, auch für dieselbe Version (z.B. für einen erneuten Versand nach einer Korrektur); frühere Entwürfe/Sendungen bleiben erhalten. Optionales Feld kind: "standard" (Default, mit PDF-Anhang) oder "eskalation" (Betreff #VA# Ausbleibende RUEM-VA;<VA-ID>; MAIL_NN, Text nach fester Vorlage, ohne PDF-Anhang für die Eskalation aus der Eskalationsliste, siehe oben).

Empfänger (Name + Adresse) werden dreistufig aufgelöst, jede Stufe nur als Fallback zur vorherigen:

  1. VA-Mail/Fax-Kontakt aus dem EKP-Portal-Sync (ekp_va_contact) der Kanal, über den laut Portal tatsächlich per Mail/Fax vorabgestimmt wird; bei mehreren rollenspezifischen Einträgen wird der bevorzugt, dessen Beschreibung „abgebend“ erwähnt (wir sind bei einer VA-Anfrage selbst aufnehmend).
  2. Granularer Clearing-Kontakt (Thema „2.1 Vorabstimmung“, bei kind: "eskalation" bevorzugt Szenario „2.1.03 Ausbleibende RUEM-VA“).
  3. Der generische Paragraph-59-TKG-Kontakt aus ekp_provider (CSV-Import bzw. Company-List-Grunddaten des Sync).

Ist auf keiner Stufe eine E-Mail-Adresse hinterlegt, antwortet die API mit 422. Betreff/Text/Anhangsname werden je nach kind vorbefüllt.

curl -s -X POST \
  http://localhost:3000/v1/versions/bd416d6d-7d7b-4dfb-9921-ec4d9ccb911e/email-draft \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
// HTTP 201
{
  "id": "82029750-4bc6-4427-bbc8-637987a696fd",
  "version_id": "bd416d6d-7d7b-4dfb-9921-ec4d9ccb911e",
  "to_name": "Telekom Deutschland GmbH",
  "to_address": "ABW-Anbieterwechsel@telekom.de",
  "subject": "VA-KUE-MRN DEU.RSMBGL.V260703002",
  "body_text": "Sie erhalten hiermit eine Vorabstimmungsanfrage ...",
  "pdf_display_name": "VA-KUE-MRN DEU.RSMBGL.V260703002 8654 5769745, 7701848, 7701849 // Kern Spitz 2 83416 Saaldorf-Surheim.pdf",
  "status": "draft",
  "kind": "standard",
  "sent_at": null,
  "smtp_response": null,
  "created_at": "2026-07-03 12:06:12",
  "updated_at": "2026-07-03 12:06:12"
}

E-Mail-Entwurf bearbeiten

PUT /v1/email-drafts/:draftId vor dem Versand können Empfänger, Betreff und Text angepasst werden. Bereits versendete Entwürfe (status: "sent") können nicht mehr geändert werden (HTTP 409). Wichtig: Formularfelder im Dashboard/der Test-UI halten Änderungen zunächst nur lokal vor dem Versand wird deshalb immer erst hierüber gespeichert, sonst würde der zuletzt gespeicherte statt der gerade sichtbare Stand verschickt.

curl -s -X PUT http://localhost:3000/v1/email-drafts/82029750-4bc6-4427-bbc8-637987a696fd \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"body_text": "Angepasster Text vor Versand.\n\nMfG RSM Freilassing"}'

E-Mail senden

POST /v1/email-drafts/:draftId/send rendert das PDF frisch aus der Version und hängt es unter dem sanitisierten Dateinamen an (außer bei kind: "eskalation" dort kein Anhang), und verschickt die E-Mail per SMTP. SMTP-Host/Port/Verschlüsselung/Zugangsdaten kommen aus den Account-Einstellungen (siehe unten), leere Felder fallen auf backend/.env zurück. Ist from_email beim Account gesetzt, wird es (mit from_name) als Absender verwendet, sonst SMTP_FROM aus .env. Ist reply_to_email gesetzt, wird ein Reply-To-Header ergänzt. Ist bcc_address gesetzt, erhält diese Adresse eine BCC-Kopie. Danach status: "sent"; ein erneuter Sendeversuch liefert HTTP 409. Die letzte SMTP-Antwortzeile des Exim-Relays (typischerweise 250 OK id=<exim-queue-id>) wird in smtp_response gespeichert, um den Versand später im EXIM-Mainlog nachvollziehen zu können.

curl -s -X POST http://localhost:3000/v1/email-drafts/82029750-4bc6-4427-bbc8-637987a696fd/send \
  -H "Authorization: Bearer $TOKEN"
{
  "id": "82029750-4bc6-4427-bbc8-637987a696fd",
  "status": "sent",
  "sent_at": "2026-07-03 12:09:43",
  "smtp_response": "250 OK id=1qXXXXX-0001Yz-Fg",
  // ...
}

Im Dashboard zeigt die Versionen-Tabelle je Version alle tatsächlich versendeten E-Mails (Zeitstempel, Empfänger, bei Eskalationen zusätzlich markiert) mit smtp_response als Tooltip.

Account-Einstellungen (SMTP/Absender/Reply-To/BCC)

GET /v1/account liefert die aktuellen Einstellungen anhand des Bearer-Tokens. SMTP-Zugangsdaten (smtp_*) hängen am Account und gelten damit für alle Token/Nutzer desselben EKP gemeinsam (gemeinsam genutzte Infrastruktur). Absender/Reply-To/BCC (bcc_address, from_name, from_email, reply_to_name, reply_to_email) hängen dagegen am Bearer-Token: mehrere Mitarbeiter/Systeme desselben EKP teilen sich einen Account (ITU Carrier Code, Anbietername, Vorabstimmungs-ID-Zähler), senden aber jeweils mit ihrer eigenen Absenderadresse dafür braucht jeder Nutzer einen eigenen Token (siehe scripts/create-token.js). smtp_password wird nie zurückgegeben, nur smtp_password_set (bool).

curl -s http://localhost:3000/v1/account -H "Authorization: Bearer $TOKEN"
{
  "smtp_host": null, "smtp_port": null, "smtp_secure": null, "smtp_user": null,
  "smtp_password_set": false,
  "bcc_address": "michael.rack@rsm-freilassing.de",
  "from_name": "RSM Freilassing", "from_email": "anbieterwechsel@rsm-freilassing.de",
  "reply_to_name": null, "reply_to_email": null
}

PUT /v1/account aktualisiert nur die im Body enthaltenen Felder (Feld weglassen = unverändert lassen; leerer String löscht den Wert → Fallback auf .env-Default bzw. kein Header/keine BCC). Alle Felder sind optional. smtp_password nur bei Änderungswunsch mitgeben (nicht-leerer Wert), sonst weglassen ein leerer String bei smtp_password wird ignoriert (kein Löschen über die API, dafür direkt in der DB).

Feld Bedeutung
smtp_host, smtp_port, smtp_user, smtp_password SMTP-Zugangsdaten, leer = .env-Default
smtp_secure bool, leer/null = .env-Default
bcc_address erhält eine Kopie jeder gesendeten E-Mail
from_name, from_email Absender (From-Header); from_email leer = .env SMTP_FROM
reply_to_name, reply_to_email optionaler Reply-To-Header
curl -s -X PUT http://localhost:3000/v1/account \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "bcc_address": "michael.rack@rsm-freilassing.de",
    "from_name": "RSM Freilassing",
    "from_email": "anbieterwechsel@rsm-freilassing.de"
  }'

Ungültige E-Mail-Adressen oder ein ungültiger Port liefern HTTP 400 mit {"error":"validation_failed","errors":[{"field":"...","message":"..."}]}.

In der PHP-Test-UI: account_settings.php.

Weiteren Nutzer/Token für denselben Account anlegen (z.B. ein zweiter Mitarbeiter desselben EKP mit eigener Absenderadresse, aber gleichem Carrier Code/Zähler):

cd backend
node scripts/create-token.js --carrier-code=RSMBGL --label="Erika Musterfrau"

Der neue Bearer-Token ist unabhängig vom bestehenden Token nutzbar; PUT /v1/account mit diesem neuen Token setzt dessen eigene Absender/Reply-To/BCC- Einstellungen, ohne die des ursprünglichen Tokens zu verändern. GET /v1/vorabstimmungen, die Vorabstimmungs-ID-Zählung usw. sind für beide Token identisch (gleicher Account).


Datenmodell (Kurzüberblick)

  • vorabstimmung_process ein Datensatz pro Vorabstimmungs-ID (Mutter-Prozess). UNIQUE auf (account_id, vorabstimmungs_id) die VA-ID ist nur je EKP eindeutig, nicht plattformweit.
  • vorabstimmung_version jede Änderung/jeder Schritt als eigene versionierte Zeile (F1F66), referenziert immer den Mutter-Prozess.
  • vorabstimmung_version_rufnummer Rufnummernlisten je Version, getrennt nach section (anfrage/antwort).
  • ekp_provider EKP-Stammdaten: Basis-Import aus der CSV-Firmenliste (bnetza, itu_carrier_code, generischer §59-TKG-Kontakt) plus Handelsregister/Anschrift aus dem EKP-Portal-Sync (siehe unten).
  • ekp_clearing_contact / ekp_clearing_scenario granulare Clearing-Kontakte je Thema (2.1 Vorabstimmung … 2.6 Verbraucheranfragen) mit verknüpften Szenario-Codes aus dem AK-SPRI-Clearing-Handbuch.
  • ekp_va_contact Kontakte für „Contact details for provider change … by mail or fax“ (Empfängerauflösung Stufe 1 beim E-Mail-Versand).
  • ekp_portident / ekp_foreign_portident vom EKP selbst verwendete bzw. bei anderen (fremden) EKP referenzierte Portierungskennungen.
  • email_draft E-Mail-Entwürfe je Version (mehrere pro Version möglich), kind (standard/eskalation), status (draft/sent/failed), smtp_response (Exim-Antwortzeile nach Versand).

Details siehe db/README.md und die SQL-Dateien in db/schema/.

EKP-Portal-Sync

backend/scripts/sync-ekp-portal.js synchronisiert Firmenstammdaten, granulare Clearing-Kontakte und die Firmen-Detailseite (Anschrift, Portierungskennungen, VA-Mail/Fax-Kontakte) aus dem EKP-Portal (cockpit.xc.en.enghousehosted.com/ekp-portal/). Das Portal ist eine serverseitig gerenderte Vaadin-8-Anwendung ohne REST-API die sichtbare WSS-Kommunikation ist Vaadins internes UIDL-Zustands-Diff-Protokoll mit sitzungsgebundenen Komponenten-IDs und daher zu instabil, um sie direkt zu parsen. Stattdessen steuert das Skript einen echten headless Chromium-Browser (Playwright) und liest das gerenderte DOM anhand sichtbarer Feld-Labels aus.

Zugangsdaten in backend/.env: EKP_PORTAL_URL, EKP_PORTAL_USER, EKP_PORTAL_PASSWORD.

node scripts/sync-ekp-portal.js all              # scrapen + committen in einem Rutsch
node scripts/sync-ekp-portal.js scrape --pages=1-8 --out=ekp-sync.json   # nur Firmenliste + Clearing-Kontakte
node scripts/sync-ekp-portal.js detail --range=1-761 --out=ekp-sync.json # Detailseiten ergaenzen
node scripts/sync-ekp-portal.js commit --in=ekp-sync.json                # Checkpoint in die DB schreiben

scrape/detail schreiben eine JSON-Checkpoint-Datei (dedupliziert nach BNetzA-Reg.-Nr., bereits erfasste Firmen werden bei erneutem Aufruf übersprungen) ein Lauf über alle ~770 Firmen dauert je nach Modus 2090 Minuten und lässt sich dadurch in mehreren Etappen durchführen und fortsetzen (--force erzwingt eine erneute Erfassung bereits vorhandener Firmen). commit ersetzt den Inhalt der betroffenen Tabellen vollständig (DELETE+INSERT in einer Transaktion) das Portal ist die alleinige Quelle der Wahrheit für diese Daten. Aktuell nur manuell auslösbar, kein Cron-Job.

Roadmap

  • Bilaterale 2-Schritt-Variante (Zusammenlegen von Schritt 1 und 3, nach vorheriger Vereinbarung zwischen zwei EKP)
  • Automatischer Mailversand der Eskalationsliste an ein zentrales Postfach (aktuell nur CSV-Export über GET /v1/eskalationen?format=csv, bzw. Einzelversand über die Eskalations-E-Mail-Funktion je Vorgang)
  • Mehrsprachigkeit / Internationalisierung der Test-UI
  • EKP-Portal-Sync als planbarer Cron-Job statt nur manuell auslösbar
  • Clearing-Kontakte der Themen 2.2/2.42.6 auch für die Empfängerauflösung nutzen (aktuell nur 2.1 Vorabstimmung verdrahtet)