P2503 · Implementation Plan · Projektpartnerportal

Projektpartnerportal Self-Service — Implementation Plan

active 2026-06-15 · PRD · Story-Hub · ADR-0052 · Jira P2503-226
Methodik: Tracer-Bullet-Slices — jeder Slice ist ein dünner vertikaler Schnitt durch alle Layer (Schema, Config, Logik, UI, Tests) und für sich demobar. Alle Slices sind AFK (ohne menschliches Merge-Gate baubar). Offene Fachbereichs-Punkte (Migrations-Umfang, SF-Empfänger) sind über dokumentierte Defaults + Config-Werte aufgelöst, nicht über Gates — der Fachbereich zieht später nur den Config-Wert nach.

Slice-Übersicht

Dependency-getriebene Reihenfolge. Fundament-Slices (0a–0c) laufen parallel; der kritische Pfad ist 0a → 2 → 3 → 4 (Registrierungs- und Verifizierungs-Flow).

PhaseTitleTypeBlocked byJira-Ticket
0aDatenmodell: drupal/profile + Rolle + Felder + field_ppp_idAFKP2503-227
0bTest-Harness: FunctionalJavascript + Chromedriver in DDEVAFKP2503-240
0cForm-Primitive-SDCs (Inputfield/Textarea/Dropdown/Upload)AFKP2503-123…126
1Login mit E-MailAFK0aP2503-230
2Private Uploads (owner-only)AFK0aP2503-229
3Registrierungs-Wizard (Webform)AFK0a, 0c, 2P2503-228
4E-Mail-Flows (Verifizierung/Willkommen/Reset)AFK3P2503-231
4bBestandsnutzer-Aktivierung: Passwort-Policy + Willkommens-MailAFK4, 8P2503-241
5Profil-Edit (native Profile-Form)AFK0a, 0cP2503-232
6Rollen-Trennung (Verifikation)AFK0a, 5P2503-234
7Account-Löschung (DSGVO)AFK0a, 2P2503-233
8Migration (SqlBase, Neos→Drupal)AFK0a, 2P2503-235
9Salesforce-Sync per Mail (Profil-Diff)AFK0a, 5P2503-239
10Suchagent / Projekt-Alert (Prio hoch)AFK0a, ProjektlisteP2503-237
11Merkliste-Upgrade (auf bestehender Lösung)AFK0a, Merkliste, ProjektlisteP2503-238

Slice-Details

0a — Datenmodell (drupal/profile)

Composer-Require drupal/profile (1.14.0) + drupal/entity (1.6.0). Profiltyp projektpartner mit allen vier Feldgruppen (Person/Kontakt/Verfügbarkeit/Qualifikation) in config/sync. Rolle projektpartner mit Permissions (eigenes Profil ansehen/bearbeiten, eigenen Account löschen). field_ppp_id (Integer, unique, nullable) auf der User-Entity. Consent-Feld mit Timestamp+Version. field_skills ist ein Taxonomie-Referenz-Feld (neues Vokabular „skills"). Datenmodell-Entscheidung: ADR-0052. Kernel-Test für das Schema.

  • Profiltyp projektpartner + Rolle existieren in config/sync, drush config:status sauber
  • Alle vier Feldgruppen als Profilfelder; field_ppp_id auf User
  • Consent-Feld speichert Annahme mit Timestamp + Version
  • Kernel-Test grün; Modul-Dependency in info.yml deklariert

0b — Test-Harness (FunctionalJavascript)

.ddev/docker-compose.testing.yaml mit Chromedriver-Service (Image drupalci/webdriver-chromedriver), MINK_DRIVER_ARGS_WEBDRIVER + SIMPLETEST_* Env. Ein Referenz-WebDriverTestBase-Test, der den Stack beweist. Querschnitt — früh aufsetzen, da alle Formular-Slices darauf testen. Playwright bleibt per Repo-Policy ausgeschlossen.

  • Chromedriver-Service startet mit DDEV; Env-Vars gesetzt
  • Ein FunctionalJavascript-Referenz-Test grün lokal in DDEV
  • Test läuft in der Azure-DevOps-Pipeline grün

0c — Form-Primitive-SDCs

Inputfield, Textarea, Dropdown, Upload-field als SDCs im byte_theme (eigene Tickets P2503-123…126, ready-for-dev). Dependency für alle Portal-Formulare; läuft theme-seitig parallel zum Fundament. Checkbox/Radio/Password sind eine bekannte Design-Lücke und ggf. zu ergänzen.

  • Vier Form-Primitive als SDC mit component.yml + Kernel-Render-Test
  • Checkbox/Radio/Password-States geklärt (Figma oder Core-Forms-Styling)

1 — Login mit E-Mail

login_emailusername (bereits aktiv) konfigurieren. Standard-Drupal-Login der Redakteure bleibt funktionsfähig. Verständliche Fehlermeldung bei Fehllogin.

  • Login per E-Mail + Passwort funktioniert
  • Redakteur-Standard-Login unverändert
  • FunctionalJavascript-Test für Login-Form

2 — Private Uploads (owner-only)

private://-Upload-Feld (private://ppp/<uid>/), erlaubte Typen + Größenlimit. hook_file_download / Profile-Access-Handler erzwingt Ownership-Check. Pattern wie PrivateFileUriItemOverride in solcom_vacancy_application.

  • Datei nur für Eigentümer + Admin abrufbar; Fremde → 403 (Test belegt)
  • Kein öffentlicher Pfad
  • Einzeln entfernbar, auch im Wizard (Draft-File-Handling)

3 — Registrierungs-Wizard (Webform)

3-seitige Webform (Kontakt&Unterlagen Pflicht / Verfügbarkeit / Qualifikation optional), Vor/Zurück, Honeypot, Per-Page-Validierung. Draft-Submission als Zwischenstand (Files vor User-Existenz). Final-Submit-Handler legt User + projektpartner-Profil + Rolle atomar an (gesperrt bis Verifizierung), hängt Dateien um. Verwaiste Drafts per Cron-Purge.

  • 3 Seiten mit Navigation; Passwort-Match + Consent-Checkboxen Pflicht
  • Final-Submit erzeugt User+Profil+Rolle atomar, Account gesperrt
  • Doppelte E-Mail + Honeypot abgefangen
  • FunctionalJavascript-Test über alle drei Schritte inkl. Upload

4 — E-Mail-Flows

easy_email-Templates (Verifizierung, Willkommen, Passwort-Reset) im SOLCOM-Layout, in config/sync versioniert. Double-Opt-in aktiviert den Account. Migrierte Partner erhalten beim ersten Login einen erzwungenen Reset.

  • Verifizierungs-Mail nach Registrierung; Klick aktiviert Account
  • Passwort-Reset-Flow via easy_email-Template
  • Templates in config/sync; Willkommens-Mail nach Aktivierung

4b — Bestandsnutzer-Aktivierung (Passwort-Policy + Willkommens-Mail)

Neue Dependency drupal/password_policy + Submodul password_policy_expiration (D11-Auflösung ohne Konflikt geprüft, Version beim Bau pinnen). Migrierte Accounts (aus Slice 8) bekommen ein abgelaufenes Passwort-Datum → beim ersten Login erzwingt Drupal „Passwort abgelaufen, bitte neu setzen". Zusätzlich ein Drush-Batch-Command, der zum Go-Live an alle migrierten Partner eine Willkommens-Mail mit Einmal-Login-Link (user_pass_reset, easy_email) versendet. Allgemeine Passwort-Richtlinie (Mindestlänge/Komplexität) gilt damit für alle.

  • password_policy + Expiration installiert + konfiguriert
  • Migrierter Account: erster Login erzwingt Passwort-Wechsel (Functional-Test)
  • Batch-Command versendet Willkommens-Mail mit Einmal-Login-Link an migrierte Partner
  • Nach Reset normaler Zugang; allgemeine Policy greift für alle Nutzer

5 — Profil-Edit (native Profile-Form)

Native drupal/profile-Edit-Route /user/{uid}/projektpartner. Feldliste einmal als Profilfelder definiert; Webform spiegelt nur die Erstanlage. Zugriffsschutz: nur eigenes Profil.

  • Alle Profilfelder + Unterlagen editierbar; Validierungen greifen
  • Partner sieht/bearbeitet nur eigenes Profil (Fremd → 403)
  • FunctionalJavascript-Test für Edit-Form

6 — Rollen-Trennung (Verifikation)

Folgt strukturell aus 0a + 5 (separate Profile-Route). Verifizieren: Redakteure/Admins behalten /user/{uid}/edit, Wizard nur für Selbstregistrierung erreichbar.

  • Nutzer ohne projektpartner-Rolle sehen Standard-Account-Formular
  • Wizard für redaktionelle Nutzeranlage nicht erreichbar

7 — Account-Löschung (DSGVO)

Self-Service-Löschung mit Bestätigungs-Schritt. user_cancel_delete + hook_user_cancel (Pattern wie ApplicationPurger): entfernt Account + Profil + alle privaten Dateien + Verzeichnis.

  • Löschung mit Bestätigungs-Schritt
  • Profil + Files + Verzeichnis unwiderruflich entfernt; PII-frei (Test belegt)

8 — Migration (SqlBase, Neos→Drupal)

SqlBase-Source direkt gegen den Neos-Dump (temporäre DB-Connection). Zwei verkettete Migrations: User (inkl. field_ppp_id) vor Profil via migration_dependencies/migration_lookup. DC2Type:array-Felder per callback-unserialize. Datei-Migration über Joins in private://. Passwörter nicht migriert → force-reset. Umfang: alle Bestandspartner (ungefiltert; DSGVO-Retention-Nachbewertung nach Go-Live). Freitext-Skills werden auf die Skill-Taxonomie normalisiert (Fuzzy-Mapping; unbekannte Terme landen in einem Review-Report).

  • User + Profil koordiniert angelegt; field_ppp_id für Idempotenz
  • Dateien dem richtigen Partner zugeordnet
  • Freitext-Skills auf Taxonomie-Terme normalisiert; unbekannte Terme im Review-Report
  • Idempotent (rollback/update-fähig); Trockenlauf-/Validierungsbericht liegt vor
  • Alle Datensätze migriert (kein Consent-Filter im Bau)

9 — Salesforce-Sync per Mail (Profil-Diff)

hook_ENTITY_TYPE_update auf projektpartner-Profil vergleicht $entity->original, ermittelt geänderte Felder, legt bei Änderung ein Queue-Item an; QueueWorker mit injiziertem MailManager versendet via easy_email eine Diff-Mail (alt → neu) an einen konfigurierbaren Empfänger. Pattern wie SalesforceUrlWriteback; beim SF-Go-Live wird der Worker gegen einen SF-PATCH getauscht. Default-Empfänger: Config-Platzhalter, vom Fachbereich nachzutragen — blockiert den Bau nicht.

  • Profil-Änderung → genau eine Diff-Mail mit geänderten Feldern
  • Speichern ohne Änderung → keine Mail, kein Queue-Item
  • Empfänger als Config-Wert; Kernel-Test für Diff + Mail (Mock-MailManager)

10 — Suchagent / Projekt-Alert (Prio hoch)

Pro Partner ein kriterienbasierter Alert (Skills + Regionen). Cron matcht neue project_offer gegen Kriterien → easy_email-Benachrichtigung (Pattern wie solcom_project_offer_alert-Digest). Migration der ~20.000 Bestands-Kriterien; die Alert-Skill-Begriffe werden auf dieselbe Skill-Taxonomie gemappt wie die Profile, sodass das Matching gegen Taxonomie-Terme läuft. Gesetzt, nicht optional.

  • Alert anlegen/bearbeiten/(de)aktivieren/löschen; ein Alert pro Partner
  • Cron-Mail bei Treffer, keine Mail ohne Treffer
  • Bestands-Kriterien migriert, Vokabular gemappt
  • FunctionalJavascript-Test für die Alert-Form

11 — Merkliste-Upgrade

Bestehende Frontend-Merkliste (WatchlistController, watchlist.js localStorage, SDCs watchlist-toggle/watchlist-bar) für eingeloggte Partner backend-persistent erweitern (geräteübergreifend, ohne 5er-Limit). Anonyme behalten localStorage; beim Login werden lokale Einträge übernommen. Refactor, kein Neubau — bestehende SDCs/Controller erweitern.

  • Eingeloggt: Merkliste backend-persistent, geräteübergreifend, ohne Limit
  • Anonym: localStorage-Verhalten bleibt; Login übernimmt lokale Einträge
  • Bestehende SDCs/Controller erweitert, nicht ersetzt
  • FunctionalJavascript-Test für Toggle + Login-Merge

QA-Plan

Click-Pfade pro Akzeptanzkriterium — Quelle der Wahrheit für die Pre-PR-Acceptance-Stage des Ralph-Loops und die Mensch-QA nach PR-Open (ADR-0037). Basis-URL https://solcom.ddev.site. Routen mit * in der Implementierung final zu bestätigen. Identisch gepflegt im Story-Hub.

AK-#URL / PfadActionExpected
AK-1/projektpartnerportal/registrieren *Wizard Seite 1→2→3 + „Zurück"Drei Seiten, Progress, Vor/Zurück erhält Werte
AK-2/projektpartnerportal/registrieren *(a) Passwort ≠ Wdh (b) Consent leer (c) korrekt(a) Match-Fehler (b) Submit blockiert (c) Seite 1 ok
AK-3nach Final-Submit + drush user:informationRegistrierung abschließenUser+Profil+Rolle angelegt, Account gesperrt, Consent mit Timestamp
AK-4Test-Mailbox + Verifizierungs-LinkVerifizierungs-Mail, Aktivierungs-Link klickenMail im SOLCOM-Layout; Klick aktiviert Account
AK-5/user/login(a) korrekt (b) falsches Passwort(a) eingeloggt (b) verständlicher Fehler; Redakteur-Login bleibt
AK-6/user/passwordReset auslösen, Link klickenReset-Mail via easy_email; neues Passwort funktioniert
AK-7/projektpartnerportal/registrieren * (DevTools)(a) doppelte E-Mail (b) Honeypot füllen(a) Fehler, kein 2. Account (b) schein-ok, kein User
AK-8/user/{uid}/projektpartnerAlle Feldgruppen bearbeiten + speichernEditierbar; Zeichenlimits greifen; persistiert
AK-9private File-URL(a) Eigentümer (b) anderer Partner (c) anonym(a) Datei (b) 403 (c) 403; kein public path
AK-10/user/{uid_fremd}/projektpartnerFremdes Profil aufrufenHTTP 403
AK-11Account-Bereich → Löschen * + ls private://ppp/<uid>/Löschung mit BestätigungAccount+Profil+Dateien+Verzeichnis weg; keine PII auffindbar
AK-12/user/{uid}/edit als RedakteurAccount-Formular öffnenStandard-Drupal-Formular, keine Profilfelder
AK-13drush migrate:import solcom_projektpartner_user,…_profileMigration ausführenUser+Profil je Partner; field_ppp_id; DC2Type:array deserialisiert
AK-14ls private://ppp/<uid>/ nach MigrationDatei-Migration prüfenBestands-Dokumente korrekt zugeordnet
AK-15/user/login als migrierter PartnerErstes LoginErzwungener Passwort-Reset
AK-16migrate:import --update + migrate:rollback2× laufen + rollbackIdempotent; sauberer Rollback; Validierungsbericht
AK-17Account → Suchagent * + drush cron + MailboxKriterien anlegen, passendes Projekt publizieren, CronMail bei Treffer, keine bei Nicht-Treffer
AK-18drush migrate:import solcom_projektpartner_alertAlert-MigrationMigrierte Alerts aktiv; Vokabular gemappt; ein Alert/Partner
AK-19/projektportal/merkliste (anonym, dann eingeloggt)(a) anonym merken (b) einloggen (c) 2. Gerät(a) localStorage bleibt (b) lokale Einträge übernommen (c) geräteübergreifend, kein 5er-Limit
AK-20/user/{uid}/projektpartner + SF-Sync-Mailbox(a) Feld ändern+speichern (b) ohne Änderung speichern(a) genau eine Diff-Mail (alt→neu), Queue-Item (b) keine Mail
AK-21ddev exec ./vendor/bin/phpunit --testsuite functional-javascriptFrontend-Test-Suite gegen ChromedriverWizard-Test grün; Smoke-Tests contact_form/newsletter_signup/vacancy_application grün

Geklärte Entscheidungen (2026-06-15)

  • Migrations-Umfang: Alle Bestandspartner werden migriert (ungefiltert), maximaler Datenerhalt. DSGVO-Hinweis: nie-aktivierte / consent-lose Accounts sind nach Go-Live über eine Retention-Policy rechtlich nachzubewerten.
  • SF-Sync-Empfänger: Config-Platzhalter; echte Adresse wird vor Go-Live nachgetragen (blockiert den Bau nicht).
  • Skill-Vokabular: field_skills als strukturierte Taxonomie/Tags (nicht Freitext). Konsequenz: Datenmodell (0a) legt ein Skill-Vokabular an, Migration (8) normalisiert die ~37.700 Neos-Freitext-Skills auf Taxonomie-Terme (Fuzzy-Mapping + Review unbekannter Terme), Suchagent (10) matcht gegen die Taxonomie.
  • UI-Label nach Login: „Mein Projektpartnerprofil".
  • Echter SF-PATCH-Writeback — Phase 2 (nach SF-Go-Live); bis dahin Mail.