| .claude | ||
| .claude_attachments | ||
| backend | ||
| dashboard | ||
| db | ||
| ui-php | ||
| .gitignore | ||
| README.md | ||
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 KlasseAnbieterwechsel\ApiClientmit 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/Anschriftportidents– 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 F1–F66 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 (F7–F10) |
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 (F14–F17) |
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 (F27–F29) |
fenster_sonstiges_text |
string | F30, Pflicht wenn fenster_sonstiges=true |
rueckinfo_ansprechpartner, rueckinfo_fax_email, rueckinfo_tel |
string | F31–F33 |
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 nuroutcome: "ablehnung"zulässig (ABBM-TR); ein Zustimmungsversuch liefert HTTP409(not_applicable). - letzte Version ist eine Terminverschiebung (
terminverschiebung, TVS-VA) → bei Zustimmung ist ausschließlichzwavorgesehen (nat/adasind hier ein Fehler), bei Ablehnung ausschließlichsonmit 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 istgrund_textPflicht). 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: F39–F41 (bei TVS-VA nur zwa zulässig) |
bestaetigter_wechseltermin, ist_technologie, wita, spri, wita_vertragsnummer |
Bei Zustimmung: F42–F46 (nicht bei TVS-VA/Storno) |
adf, kni, vae, rng, wai, aif, son |
Bei Ablehnung: F48–F54 (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
VorabstimmungsIdunbekannt, wird eine neue eingehende Anfrage angelegt (wiePOST /v1/vorabstimmungen/incoming). - Ist sie bekannt, wird automatisch die passende Folgeversion auf dem
bestehenden Prozess erstellt: Storno (falls
StornierungsIdgesetzt), Terminverschiebung (fallsAenderungsIdgesetzt), sonst je nach Feldern und aktuellem Stand des Prozesses eine Schritt-2-Antwort (ZWA/NATbzw.ADF/KNI/VAE/RNG/WAI/AIF/SONgesetzt) oder eine AKM-TR-Mitteilung (assumption_of_resourcesgesetzt, 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/right→antwort_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:
- 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). - Granularer Clearing-Kontakt (Thema „2.1 Vorabstimmung“, bei
kind: "eskalation"bevorzugt Szenario „2.1.03 Ausbleibende RUEM-VA“). - 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 (F1–F66), referenziert immer den Mutter-Prozess.vorabstimmung_version_rufnummer– Rufnummernlisten je Version, getrennt nachsection(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
20–90 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.4–2.6 auch für die Empfängerauflösung nutzen (aktuell nur 2.1 Vorabstimmung verdrahtet)