P2503 · Karriere · F-100

Bewerber-Flow

REFINED P2503-90 Erstellt: 2026-05-11 · Refined: 2026-05-25 · Re-Refined: 2026-05-29 (Pivot C+ → A)

User Story

Als Bewerber möchte ich von einer Stellenausschreibung direkt zum Bewerbungsformular gelangen, damit der Bewerbungsprozess einfach und ohne Medienbruch abläuft.

Outcome

Bewerber landet von /karriere/<slug> mit einem Klick auf einer Drupal-nativen Bewerbungsseite (Sub-Route /karriere/<slug>/bewerben), füllt alle Felder analog zum bestehenden Apply-Form auf karriere.solcom.de aus (inkl. CV-Upload), bestätigt Submit, sieht eine Drupal-Erfolgsseite, und bekommt anschließend eine Personio-Bestätigungsmail. Die Bewerbung erscheint im Personio-Recruiting-Backend wie eine via Personio-Hosted-Page abgegebene Bewerbung.

Status

RE-REFINED 2026-05-29 · Pivot C+ → Pfad A. Personio hat SOLCOM den Multiposting-Partner-Scope gewährt (Sub-Task P2503-191, abgeschlossen). Damit ist der ursprünglich gewünschte Pfad A (direkter HTTP-POST gegen die Personio Recruiting API) erreichbar geworden. Die Architektur wird darauf umgestellt — dokumentiert in ADR-0050, das ADR-0047 (Email-Inbox-Bridge, Pfad C+) supersedet. Der Submit läuft asynchron über die Core-Queue; der gesamte Webhook-ACK-Layer entfällt, weil die API-Response die Application-Erstellung synchron per HTTP-2xx bestätigt. Hintergrund: Die C+-Implementierung fiel im Review vom 2026-05-28 mit Acceptance: FAIL durch (HARD-Findings S-5/S-6/S-7); die Re-Grill leitet die State-/Queue-/Security-Schicht Drupal-way neu her und vermeidet diese Findings strukturell.

CONTRACT-FIX 2026-05-30. Live-Probes haben den zuerst implementierten Contract widerlegt: /v2/auth/token mit papi-Credentials wird vom Applications-POST abgelehnt, multipart/form-data ist auf /v1/recruiting/applications falsch, und X-Company-ID ist Pflicht. Der korrekte Pfad ist: direkter Recruiting-Access-Token als Bearer, Dokumente zuerst via POST /v1/recruiting/applications/documents, danach POST /v1/recruiting/applications als JSON mit File-Referenzen. Die Live-Queue-Probe akzeptierte eine Testbewerbung mit 2xx und löste den GDPR-Purge aus; der Response-Body enthielt keine nutzbare Application-ID, und API-Read-back ist mit diesem Token nicht verfügbar (v2 404, v1 GET 405). Der Client, die Kernel-Tests und diese Story wurden entsprechend korrigiert.

Architektur (Pfad A — direkter Personio-API-POST über async Core-Queue)

Source-of-truth: ADR-0050 (supersedes ADR-0047).

Submit-Flow

  1. Bewerber öffnet /karriere/<slug>/bewerben über den Apply-CTA auf der Vacancy-Detail-Seite.
  2. Drupal-Sub-Route lädt die Vacancy-Node, rendert Stellenkontext-Header und Webform vacancy_application mit injizierten hidden fields (Personio-Job-ID, Stellentitel, Vacancy-Node-ID).
  3. Bewerber füllt Felder aus, lädt CV (Pflicht) und optional Anschreiben/weitere Anhänge hoch, bestätigt Datenschutz-Checkbox, klickt Submit.
  4. Drupal-Webform-Submit-Handler speichert die Submission inkl. Files temporär in private://applications/<submission-uuid>/, setzt status = queued (skalares Hidden-Element), schiebt ein minimales Queue-Item ({submission_id}) in solcom_personio_application_queue, navigiert auf /karriere/<slug>/bewerben/danke.
  5. QueueWorker (cron-getriggert oder via drush queue:run) lädt die Submission und delegiert an den PersonioApplicationClient-Service: Auth via direktem Recruiting-Access-Token aus key.key.solcom_personio_api_key plus X-Company-ID; Files werden zuerst an POST /v1/recruiting/applications/documents gesendet, danach folgt POST /v1/recruiting/applications als JSON mit Feldern + File-Referenzen und deterministischem Idempotency-Key (= recomputeter tracking_hash aus Submission-UUID + Site-Salt).
  6. Bei 2xx: Personio nimmt die Application synchron an; eine Application-ID im Response-Body ist optional. Drupal schreibt einen PII-freien Watchdog-Audit-Eintrag (tracking_hash, optional personio_application_id, Timestamp) und purged sofort: $submission->delete() + file_usage-Cleanup + private://applications/<uuid>/-Verzeichnis (Conditional-Retention, DSGVO-minimal).

Failure-Pfade (status-differenziert)

  • Submit-Handler-Failure (DB-down, Disk-voll): Submission wird abgelehnt mit Drupal-Inline-Error, kein Queue-Item erzeugt.
  • Transient (5xx / Netzwerk / Timeout): bis zu 5 Retries mit Backoff via DelayedRequeueException($delay) aus dem Schedule 1 min/5 min/15 min/1 h/6 h; retry_count wird durable im Submission-Element inkrementiert (nicht in der Payload — delayItem() friert sie ein).
  • Rate-Limit (429): Retry-After-Header als Delay honorieren (statt des festen Schedules).
  • Permanent (4xx außer 429: 400/401/403/422): kein Retry-Burn — sofort status = dlq_persistent + HR-Notification-Mail an hr@solcom.de mit Submission-Details. Submission + Files bleiben persistent als HR-Recovery-Layer.
  • DLQ-Erschöpfung (5× transient gescheitert): wie permanent — dlq_persistent + HR-Notification.

Idempotenz

Ein API-POST ist nicht fire-and-forget wie eine Inbox-Mail: ein Retry nach Timeout (erster POST erfolgreich, Response verloren) würde sonst eine zweite identische Application anlegen. Gegenmittel: deterministischer Idempotency-Key (= recomputeter tracking_hash) als POST-Header. Ob Personio den Header server-seitig dedupliziert, bleibt ein Retry-Szenario; ein Pre-POST-Existence-Check per API ist mit dem aktuellen Token nicht verfügbar. Der tracking_hash wechselt damit die Rolle — von Webhook-Match-Key (C+) zu Idempotenz-Key (A).

Smoke-Test

Drush-Command drush solcom:personio:smoke-test erzeugt eine markierte Test-Application (Bewerber-Name „Smoke Testkandidat", Email test+<timestamp>@solcom.de, Job-Referenz Dummy-Stelle 2635453) und durchläuft den vollständigen API-POST-Submit-Flow. Verifikation läuft über HTTP-2xx plus Drupal-GDPR-Purge; API-Read-back ist best-effort und mit dem aktuellen Token nicht nutzbar. Cleanup der Personio-Testbewerbung erfolgt manuell im Recruiting-Backend.

Notfall-Fallback (Pfad B — Hosted-Page-Redirect)

Falls die Personio-API strukturell unerreichbar wird (z. B. Scope-Entzug oder anhaltender Ausfall), faltet F-100 auf den ursprünglichen Scope zurück: Apply-CTA leitet via State-Flag state:solcom_personio.apply_redirect_enabled (Drush-Toggle, Engineering-only) auf https://solcom-gmbh.jobs.personio.de/job/<id>. User-Story-Outcome „kein Medienbruch" entfällt dann, aber Bewerbungen werden weiter angenommen. Nicht aktiv verfolgt, nur als Fallback dokumentiert.

Gegrillte Architektur-Entscheidungen (Pfad A — Detail in ADR-0050)

AchseEntscheidungBegründung
Webform-Topologie Eine geteilte Webform vacancy_application, Vacancy-Kontext (Personio-Job-ID, Stellentitel, Vacancy-Node-ID) per hook_webform_submission_form_alter aus Route-Parameter als hidden fields injiziert. Skaliert ohne Mehraufwand pro neuer Personio-Stelle, Field-Schema bleibt Single-Source-of-Truth, Submissions zentral auswertbar.
Form-Placement Eigene Drupal-Sub-Route /karriere/<slug>/bewerben. Apply-CTA auf Vacancy-Detail-Page navigiert dorthin. Pfad-Pattern an pathauto /karriere/[node:title] angelehnt. Eigene Analytics-URL, Bookmark- und Share-fähig, Browser-Back funktioniert, klare IA, kein Bruch von ADR-0014 (Vacancy-Canvas-Topologie bleibt unverändert).
File-Upload-Profil Max 3 Files (CV pflicht, Anschreiben optional, weitere Anhänge optional), max 10 MB pro File, Whitelist PDF/DOC/DOCX. Files werden temporär gespeichert in private://applications/<submission-uuid>/ — Pipe-Through ist durch die Queue-Entscheidung nicht möglich, weil der Worker zeitlich vom Submit entkoppelt ist. Standard-deutscher-Markt-Erwartung. Privates Dateisystem schließt Direkt-Crawler aus. Kein ClamAV-Layer in Drupal — Personio scannt eingehende Bewerbungen selbst.
DSGVO-Profil Minimal-Konform: Pflicht-Checkbox „Ich habe die Datenschutzhinweise gelesen" mit Link, exakte Wording und Layout 1:1 aus karriere.solcom.de-Referenz übernommen. Conditional-Retention: bei HTTP-2xx der API-Response → Drupal purgt Submission-Daten + Files sofort. Bei DLQ-Eintritt → Submission + Files bleiben persistent als HR-Recovery-Layer. Kein DOI. Rechtsgrundlage Art. 6 (1) (b) DSGVO „vorvertragliche Maßnahmen", keine Einwilligungs-Pflicht für Verarbeitung selbst. Hinweispflicht Art. 13 erfüllt. Auftragsverarbeitung mit Personio ist Vorbedingung (Sub-Task).
Queue-Mechanismus Drupal Core Queue API. Webform-Handler erzeugt minimales Queue-Item ({submission_id}); Thin-QueueWorker delegiert an PersonioApplicationClient. Backoff via DelayedRequeueException($delay) (Core-verifiziert, Cron.php:211), max 5 Versuche; retry_count durable im Submission-Element (nicht in der Payload, da delayItem() sie einfriert). Zero zusätzliche Queue-Engine-Dep; deckt SOLCOM-Volumen (< 50 Submits/Tag) locker. advancedqueue wäre Over-Tooling. Fix für S-5 (Vorgänger warf RuntimeException → kein Delay).
Error-UX bei DLQ Nur HR-Notification per Mail an hr@solcom.de mit Submission-Details + Files-Pfad. User sieht initial nur die Drupal-„Bewerbung erhalten"-Seite. Bei Personio-Erfolg kommt Bestätigungsmail von Personio. Bei DLQ-Failure bleibt Submission + Files in Drupal persistent, HR übernimmt manuell. HR-Last bewusst akzeptiert, weil User-Confusion durch technische Fehler-Mails vermieden. Conditional-Retention deckt Daten-Verlust-Risiko.
DLQ-Admin-UI View auf webform_submission gefiltert auf status='dlq_persistent' via webform_views + VBO; Re-Queue/Resolve als VBO-Actions, File-Download als View-Feld. Permission manage karriere dlq an HR-Rolle. Neue Deps: drupal/webform_views + drupal/views_bulk_operations (Beta-Risiko gegen Webform 6.3-beta → Core-Controller+ConfirmForm-Fallback). POST/CSRF/Entity-Access gratis aus Contrib statt Custom-Code. Fix für S-7 (Vorgänger: state-changing GET-Routes ohne CSRF).
Spam-Protection Nur Webform Honeypot + Time-based-Heuristik. Kein CAPTCHA initial. Niedrigste User-Reibung, DSGVO-trivial. Refit auf Friendlycaptcha falls Spam-Volumen nach Launch steigt.
E2E-Test-Strategie PHPUnit-Kernel-Tests mit Guzzle-Mock für CI. Plus Drush-Command drush solcom:personio:smoke-test, der eine markierte Test-Application gegen Dummy-Stelle 2635453 erzeugt und den Submit → Queue → POST → GDPR-Purge-Pfad prüft. Manuell pre-deploy + täglicher Cron als Health-Check. Cleanup der Personio-Testbewerbung via Recruiting-Backend. Personio bietet keine Sandbox-Tenant. Production-Dummy-Stelle ist der einzige reale Pfad. API-Read-back funktioniert mit dem verifizierten Token nicht (v2 404, v1 GET 405).
Field-Mapping Verifizierter Personio-Contract: first_name, last_name, email, job_position_id als Pflichtfelder im JSON-Body; message optional; phone als System-Attribut; files als Referenzen auf zuvor via Documents-Endpoint hochgeladene Dateien (uuid, original_filename, category). Business-kritische Custom-Felder erst aufnehmen, wenn Personio-Admin die konkreten Attribute-IDs liefert. Pragmatischer Mittelweg, Upgrade-Pfad offen. Bei Pfad A direkt der API-Request-Body statt Email-Body-Konkatenation.
State-Store Lifecycle-State als getrennte, skalare Hidden-Webform-Elemente (status, retry_count, sent_at), alle '#access': false — NICHT ein _meta-Array. Webform-Submission ist der durable Record. Skalare round-trippen sauber; Array in Hidden-Field castet zu "Array" (= S-6-Bug). Custom-Entity verworfen, da deren Views/Entity-Access-Vorteil im Repo (ProjectOfferAlert) gar nicht real ist (kein list_builder/views_data).
Idempotenz Deterministischer Idempotency-Key (= recomputeter tracking_hash aus UUID + Site-Salt) als POST-Header; Personio-Dedup-Support bleibt Retry-Probe. Kein API-Read-back-Fallback mit dem aktuellen Token. Pfad-A-spezifisches Risiko: Retry nach Timeout legt sonst Duplikat an. tracking_hash wechselt Rolle: Webhook-Match (C+) → Idempotenz (A).
Failure-Klassifizierung 2xx→sent→purge · 429Retry-After · 5xx/Netzwerk→Backoff bis 5× · 4xx außer 429→sofort DLQ + HR-Notification (kein Retry-Burn). Folgt ADR-0029 (SF-Sync). Ein 4xx wird beim Retry nie erfolgreich; 6h-Warten würde HR-Benachrichtigung sinnlos verzögern.

Akzeptanzkriterien

Pfad A (Re-Refined 2026-05-29). Webhook-AKs (Subscribe, Receiver, 24h-ACK-Sweep) und die Inbox-Adressen-AK aus dem C+-Stand sind ersatzlos entfallen; neu: Idempotency-Key + Key-Modul-Credentials. Detail-Begründung in ADR-0050.

  1. Auf einer Vacancy-Detail-Seite /karriere/<slug> existiert ein Apply-CTA (Button oder sticky-apply-bar), der auf /karriere/<slug>/bewerben verlinkt.
  2. Die Seite /karriere/<slug>/bewerben rendert einen Stellenkontext-Header (Stellentitel sichtbar, Personio-Job-ID optional sichtbar) sowie die Webform vacancy_application im SOLCOM-byte_theme-Branding (Lime-Akzent, Montserrat-Headlines, ScalaSans-Body).
  3. Felder und Pflicht-Markierungen der Webform entsprechen der Referenz-Form unter karriere.solcom.de/de/jetzt-bewerben/bewerbung_<id>-1; die exakte Field-Inventur wird via agent-browser-Skill zum Implementation-Start extrahiert und in der zugehörigen docs/plans/-Datei festgehalten.
  4. CV-Upload ist Pflichtfeld, Anschreiben und weitere Anhänge sind optional. Max 10 MB pro File, Whitelist PDF/DOC/DOCX. File-Upload-Validation erfolgt clientside (Webform built-in) und serverside.
  5. Pflicht-Checkbox „Datenschutzhinweise gelesen" mit Link auf /datenschutz; Submit ohne Checkbox wird durch Webform-Validation blockiert. Wording und Layout der Checkbox sind 1:1 von der Referenz-Form übernommen.
  6. Submit erfolgt synchron mit Spinner, speichert die Submission inkl. Files temporär in private://applications/<submission-uuid>/, schiebt ein Queue-Item (nur {submission_id}) in solcom_personio_application_queue und navigiert auf /karriere/<slug>/bewerben/danke mit einer Drupal-Erfolgsseite, die explizit auf die folgende Personio-Bestätigungsmail hinweist.
  7. Webform-Honeypot ist aktiv; Submissions unter 5 Sekunden Bearbeitungszeit werden silently rejected.
  8. QueueWorker delegiert pro Item an PersonioApplicationClient, der zuerst Files an POST /v1/recruiting/applications/documents hochlädt und danach die Bewerbung als JSON an POST /v1/recruiting/applications sendet (Field-Mapping siehe Entscheidungstabelle). Bei HTTP-2xx wird eine optional zurückgegebene application_id in das skalare Submission-Element personio_application_id geschrieben; danach wird GDPR-gepurged.
  9. Failure-Klassifizierung: 5xx/Netzwerk-Fehler → Backoff via DelayedRequeueException mit Schedule [60, 300, 900, 3600, 21600] s, max 5 Versuche; 429Retry-After-Header respektiert; 4xx außer 429 → sofort dlq_persistent ohne weitere Retries. retry_count liegt durable im skalaren Submission-Element (nicht in der Queue-Payload).
  10. Jeder POST trägt einen deterministischen Idempotency-Key (= recomputeter tracking_hash aus Submission-UUID + Site-Salt). Ein Retry nach Timeout oder 5xx soll keine Duplikat-Application anlegen; ob Personio den Header server-seitig dedupliziert, bleibt separat zu verifizieren. Ein API-Existence-Check ist mit dem aktuellen Token nicht verfügbar.
  11. Bei DLQ-Eintritt (5× 5xx erschöpft oder sofort-4xx) wird Mail an hr@solcom.de mit Submission-Details + Files-Pfad versendet. Submission + Files bleiben in Drupal persistent als HR-Recovery-Layer, Status dlq_persistent.
  12. Eine View auf webform_submission gefiltert auf status='dlq_persistent' (via webform_views) listet failed Submissions mit File-Download-Feld; Re-Queue und „Manuell erledigt" laufen als VBO-Actions über POST + CSRF (kein state-changing GET). Nur sichtbar mit Permission manage karriere dlq, die der HR-Rolle zugewiesen ist.
  13. Bei HTTP-2xx werden Submission-PII-Felder + Files aus Drupal gepurged ($submission->delete() + file_usage-Cleanup + $file->delete() + Verzeichnis-Removal). Ein PII-freier Watchdog-Audit-Eintrag (nur tracking_hash + application_id + Timestamp) bleibt erhalten.
  14. Personio-API-Credential liegt im key-Modul: key.key.solcom_personio_api_key (Provider env, Env-Var PERSONIO_API_KEY) enthält den direkten Recruiting-Access-Token. personio_company_id liegt als nicht-geheimer Config-Wert in solcom_vacancy_application.settings. Keine Credentials in config/sync/ oder im Code.
  15. PHPUnit-Kernel-Tests decken ab: PersonioApplicationClient (Field-Mapping, Idempotency-Key-Bildung, Status-Klassifizierung mit gemocktem HTTP-Client), File-Handling, GDPR-Purge-Pfad. Functional-Tests: VacancyApplicationFormController, DLQ-View-Access (403 für anonym). HTTP-Calls werden gegen einen Mock-Client verifiziert, kein Live-Call im Test-Lauf.
  16. Drush-Command drush solcom:personio:smoke-test erzeugt eine markierte Test-Application gegen Dummy-Stelle 2635453, schickt sie über den vollständigen Submit → Queue → POST-Flow und verifiziert den Erfolg über HTTP-2xx plus Drupal-GDPR-Purge. Personio-Backend-Check und Cleanup bleiben manuell.
  17. AVV mit Personio (Auftragsverarbeitungs-Vertrag) liegt vor und ist in docs/context/ referenziert (Fakten-Check bei HR/Legal vor Implementation-Start).

Abhängigkeiten

  • Erledigt (Unblocker des Pivots): Personio-Multiposting-Partner-Scope für SOLCOM ist gewährt (P2503-191 done). Damit ist POST /v1/recruiting/applications mit den SOLCOM-Credentials erreichbar — die Vorbedingung für Pfad A.
  • SOLCOM-Personio-Admin stellt sicher, dass „Custom Apply URL" pro Job auf das Drupal-Sub-Route-Pattern https://solcom.de/karriere/<slug>/bewerben gesetzt werden kann (Inbox-Adresse + Mail-Parsing entfallen unter Pfad A).
  • AVV mit Personio existiert und ist HR-/Legal-seitig dokumentiert (Faktenfrage, Vorbedingung der Implementation).
  • Personio-API-Credential wird ins Drupal-Key-Storage übertragen: key.key.solcom_personio_api_key, Provider env, Env-Var PERSONIO_API_KEY in .ddev/config.local.yaml (lokal) und Mittwald-Env (production). solcom_vacancy_application.settings:personio_company_id liefert den Pflicht-Header X-Company-ID.
  • Neue Contrib-Deps: drupal/webform_views + drupal/views_bulk_operations für die DLQ-UI (AK-12). Beta-Kompatibilität gegen Webform 6.3-beta ist zum Implementation-Start zu verifizieren; Fallback ist eine Core-Controller-Route mit ConfirmForm (POST+CSRF).
  • Mail-Server-Setup im Drupal (Symfony Mailer) versendet zuverlässig an die interne HR-Adresse hr@solcom.de (DLQ-Notification, AK-11). Kein ausgehender Mail-Versand mehr an Personio — der Spam-/SPF-Pfad zum externen Personio-Postfach entfällt.
  • ADR-0014 (Vacancy-Canvas-Topologie) bleibt unverändert — Apply-CTA innerhalb existierender Section, Ziel-URL wechselt von external field_apply_url auf internal Sub-Route.
  • ADR-0035 (Personio-XML-Anonymous-Pulling) bleibt unverändert — Inbound-XML-Feed ist davon unabhängig.
  • PRD docs/prds/2026-05-02-personio-vacancy-integration-prd.html Out-of-Scope-Klausel „Bewerbungs-Formular auf Drupal" ist via ADR-0047/0050 formal aufgehoben.

Offene Fragen

Die C+-Fragen (Inbox-Import, Inbox-Parser-Verhalten, Webhook-Event-Schema) sind unter Pfad A gegenstandslos und wurden durch die API-Schema-Probes ersetzt. Alle Punkte sind Implementation-Start-Probes, keine MVP-Blocker.

  1. Geklärt: Request-Schema. Applications-POST ist JSON, nicht multipart. Files werden separat über /v1/recruiting/applications/documents hochgeladen und im Application-Body referenziert.
  2. Geklärt: Auth-/Tenant-Header. Der direkte Recruiting-Access-Token wird als Bearer genutzt; X-Company-ID ist Pflicht. Der papi-Client-Credentials-Token aus /v2/auth/token ist für diesen Write-Endpoint nicht verwendbar.
  3. Teilweise offen: Idempotency-Key-Support. Der Client sendet den deterministischen Header. Ob Personio server-seitig dedupliziert, bleibt als Retry-Szenario zu verifizieren; ein API-Read-back-Fallback ist mit dem aktuellen Token nicht verfügbar.
  4. Rate-Limiting: Liefert Personio bei 429 einen Retry-After-Header, und in welcher Einheit? Bestimmt die 429-Behandlung in AK-9.
  5. Exakte Field-Liste, Labels und Pflicht-Markierungen der Referenz-Form unter karriere.solcom.de/de/jetzt-bewerben/bewerbung_<id>-1 (Inventur via agent-browser zum Implementation-Start).
  6. Custom-Attributes: Welche Custom-Felder (Verfügbarkeit, Gehaltsvorstellung) exponiert der SOLCOM-Personio-Tenant für Applications, und unter welchen custom_attributes-Keys?

Spike-Result: Personio-API-Surface (2026-05-25)

17 Live-Probes gegen api.personio.de mit SOLCOMs vorhandenen papi--Credentials. Zusammenfassung:

  • v1-Auth (POST /v1/auth) und v2-OAuth-Client-Credentials (POST /v2/auth/token) liefern beide gültige Tokens; Scope ist personio:recruiting:read personio:recruiting:write personio:legal-entities:read personio:workplaces:read.
  • v2-Recruiting ist vollständig read-only (OpenAPI recruiting-service-v2.yaml): 9 dokumentierte Endpoints, alle GET, kein POST/PATCH/PUT/DELETE für Application-Creation.
  • POST /v1/recruiting/applicant (Singular, historisch dokumentiert) gibt HTTP 404 mit errors.no-resource-found — Endpoint ist sunset.
  • POST /v1/recruiting/applications (Plural) gibt HTTP 401 statt 404 — Endpoint existiert. OPTIONS-Discovery zeigt erlaubte Methoden GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH.
  • Getestete Auth-Patterns für POST /v1/recruiting/applications: v1-Bearer-Token (Probe 6+9, JSON), v2-Bearer-Token (Probe 4), v1-Bearer + multipart/form-data (Probe 14), X-Personio-Partner-ID/X-Personio-App-ID-Headers (Probe 11), X-API-Key-Header (Probe 17), kein Auth (Probe 16). Alle Resultate HTTP 401 — Standard-Recruiting-Credentials reichen nicht.
  • Diagnose: POST /v1/recruiting/applications ist hinter Multiposting-Partner-Scope gated und nur für autorisierte Personio-Multiposting-Partner aufrufbar.
  • Personio Career-Page-Hosting unter solcom-gmbh.jobs.personio.de liegt hinter Vercel-Security-Checkpoint und ist nicht via WebFetch/curl scrape-bar (JS-rendered).
  • Personio-Webhook-Service (/v2/webhooks) erlaubt POST/PATCH/DELETE und ist mit den vorhandenen Credentials nutzbar — relevant für Pfad-C+-ACK-Mechanik.

Die vollständige Probe-Sequenz und die HTTP-Response-Bodies stehen in der Grilling-Session-History; relevante Findings sind im Worklog-Eintrag unten zusammengefasst.

Nachprobe 2026-05-30: Der erfolgreiche Write-Contract ist Authorization: Bearer <Recruiting-access-token> plus X-Company-ID und Content-Type: application/json. multipart/form-data und application/x-www-form-urlencoded werden vor Auth mit Media-Type-Fehler abgelehnt; JSON mit papi-Bearer bleibt 401. JSON mit direktem Recruiting-Token, Company-ID und den vier Pflichtfeldern liefert HTTP 201/2xx; die Queue-Probe mit CV akzeptierte die Bewerbung, aber ohne nutzbare Application-ID im Body. Dokumente laufen separat über den Documents-Endpoint. API-Read-back ist nicht verfügbar (v2 404, v1 GET 405).

Sicherheits-Notizen

  • Credential-Rotation erforderlich: Die in der Grilling-/Probe-Session geteilten Personio-Werte wurden im Chat-Transcript exponiert und müssen vor Go-Live rotiert werden. Betroffen ist insbesondere der direkte Recruiting-Access-Token.
  • Storage-Pfad nach Rotation: key.key.solcom_personio_api_key (key-Modul, Provider env), Env-Var PERSONIO_API_KEY in .ddev/config.local.yaml (lokal, gitignored) und Mittwald-Env. Die Company-ID bleibt nicht-geheime Config.
  • Plaintext-Credentials in .ddev/config.local.yaml: Datei ist via .gitignore ausgeschlossen, aber bei Backup-Sharing oder lokalen Snapshots zu beachten. Onboarding-Template web/sites/example.settings.local.php referenziert das Muster.

Worklog

2026-05-30 · Reconcile auf User-Decisions + voller Review · handoff-continuationMarc Philipps

Contract-Reconcile + Live-Verify + 3-Achsen-Review. Ein paralleler Loop hatte den Fix mit {no-id, files-on} gebaut; auf User-Entscheidung auf {Idempotency-Key-Send-Token, files-live via Flag personio_send_documents} reconciled. Live-Auth-Mapping (Sandbox 78231): Hex-Token = v1-Schreib-Bearer (+ X-Company-ID), papi-Creds = v2-OAuth-Client-Credentials (recruiting:read/write); v2-Recruiting-Read liefert für den Tenant 404 → echte Application-ID-Korrelation erst nach Beta-Freischaltung (P90-13). Live-Feature-Re-Test (Job 2635453): Submission #3 + CV → drush queue:run → 2xx + Send-Token (nuYc…) + GDPR-Purge + Submission gelöscht. Kernel 27/217 grün, phpcs/phpstan clean. Commit 0b65c6a gepusht (review-ready, nicht gemerged). Voller 3-Achsen-Review (Report): CLEAN — 0 HARD violations, kein Scope-Creep, spec-treu; Findings = vorab-bekannte Deferrals + File-Storage-Folge-Items (AK-12 DLQ-CV-Download, flat upload_location, private://-Wrapper). Sandbox-Creds-Rotation laut User nicht nötig (VPN-only).

2026-05-30 · Personio-Contract live korrigiert · handoff-continuationMarc Philipps

Review-ready invalidiert und Contract-Fix nachgezogen. Live-E2E-Probes haben gezeigt: der ursprüngliche Client konnte nie funktionieren, weil er papi-Client-Credentials gegen /v2/auth/token tauschte, X-Company-ID fehlte und multipart/form-data direkt an /v1/recruiting/applications sendete. Korrektur: solcom_personio_api_key hält den direkten Recruiting-Access-Token, personio_company_id liefert den Pflicht-Header, Dateien werden via Documents-Endpoint hochgeladen und die Application danach als JSON erstellt. Live-Queue-Probe am 2026-05-30: Test-Submission #2 wurde via drush queue:run solcom_personio_application_queue verarbeitet, Personio akzeptierte den Write mit 2xx, Drupal löschte Submission und Test-CV via GDPR-Purge; die Response enthielt keine nutzbare Application-ID, v2/v1-API-Read-back ist nicht verfügbar. Kernel-Test PersonioApplicationClientTest auf neuen Contract umgestellt: 16 Tests / erwarteter Mock-Contract inkl. 2xx ohne ID; nur bestehende Drupal/Webform/Key-Deprecations.

2026-05-30 · Build Pfad A (Slices 184–188 + Simplify) · solcom-lfg / implementMarc Philipps

Refactor-mit-Kill-Liste autonom gebaut, PR review-ready. Fünf Commits auf mp/P2503-90: bf59fc5 P2503-184 (Tracer-Bullet Submit→Queue→API-POST), 8925a38 P2503-186 (Idempotency-Key HMAC + GDPR-Purge bei 2xx via ApplicationPurger), 544537d P2503-187 (DLQ-Recovery: Skalar-State-Fix, S-7 CSRF via ConfirmFormBase, HR-Mail via DlqNotifier), fa872cd P2503-188 (Smoke-Test + Functional-Tests + honeypot-Fix), 8e47550 refactor (DlqConfirmFormBase, Simplify-Pass DRY). Kill-Liste exekutiert: PersonioMailBuilder + PersonioWebhookController(+Route) + PersonioWebhookCommands + Tests gelöscht. Neue Deep Modules: PersonioApplicationClient, ApplicationPurger, DlqNotifier (je +Interface, modelliert auf SfClient). PR #165066develop (Titel/Description auf Pfad A aktualisiert, vorher stale „Webhook").

Verifikation: Kernel-Suite 24 Tests / 183 Assertions OK (Client Field-Mapping + Idempotency + Status-Klassifizierung gegen Mock-HTTP, Purger GDPR-Pfad, Worker S-5-Invarianten, DLQ-Listing + Confirm-Forms), phpcs clean, phpstan level-6 0 Errors. L3.PHP-Reviewer auf der GDPR-Purge-Fläche (1 BLOCKER print_rjson_encode gefixt + file_usage-count=0 GDPR-Härtung).

Non-obvious Befunde (→ Auto-Memory): (a) honeypot-Dependency-Bug — die ausgelieferte Webform-Config deklarierte dependencies.module: [honeypot], aber die info.yml nicht → jeder Fresh-Install/Functional-Test brach mit UnmetDependenciesException; Kernel-Tests maskierten das (Webform inline statt config/install-Import). (b) Webform-API-AsymmetriesetElementData() guarded gegen undefinierte Elemente (silent no-op), getElementData() liest ungeguarded; State-Schreiben modulweit auf setElementData vereinheitlicht, Form-Test config-treu gemacht. (c) Formatter-Race — PostToolUse-Formatter strippt „unused" Imports zwischen Import-Edit und Nutzungs-Edit.

Residuals (durabel im PR-Body): Functional-Suite geblockt durch pre-existing node_make_sticky_action Config-Install-Ordering im solcom_vacancy-Dependency-Tree (projektweit, out-of-scope; Tests geschrieben + korrekt); API-Read-back-/Existence-Check-Fallback deferred, weil v2 404 und v1 GET 405 liefern; Personio-Credentials müssen vor Go-Live rotiert werden. Jira-Transitions zu „Zu Review" für P2503-184/186/187/188 sind als Entwurf der Session beigelegt — kein autonomes Posten (Memory feedback_no_autonomous_jira_comments). Merge bleibt menschlich.

2026-05-29 · Re-Grill #2 — Design-Gap-Stress · solcom-grillMarc Philipps

Gelocktes Pfad-A-Design gegen den Ist-Code gestresst. Zentraler Befund: das Design ist Papier — der Code unter web/modules/custom/solcom_vacancy_application/ ist noch die gescheiterte C+-Implementierung, und alle drei FAIL-Findings sind dort live: Worker wirft \RuntimeException statt DelayedRequeueException (S-5, ApplicationQueueWorker.php:128/136/214), _meta ist ein #type:hidden-Array (S-6, webform...yml:85), DLQ-Routes sind GET ohne CSRF (S-7, routing.yml:50/61); PersonioApplicationClient existiert nicht, und /personio/webhook/application-created (_access:'TRUE') ist noch aktiv exponiert. Netto-neu aus dem Re-Grill — zwei Refinements (in ADR-0050 Consequences eingearbeitet): (G1) Lifecycle-State als separate skalare Webform-Elemente, KEIN _meta-Array — die S-6-Wurzel war der Array-Typ, nicht der co-located Ort (GDPR-Kopplung bleibt erwünscht); (G2) Re-Enqueue-Loop-Guard — der Worker muss retry_count via $submission->save() vor dem DelayedRequeueException-Throw schreiben, und der ApplicationQueueHandler darf nur beim Initial-Submit enqueuen (Guard auf gesetztes status-Element), sonst re-triggert jedes Worker-save() den Handler → Endlosschleife. Disposition (ableitbar, nicht gegrillt): Refactor-mit-Kill-ListePersonioMailBuilder/PersonioWebhookController/PersonioWebhookCommands + Webhook-Route löschen, Scaffolding refactoren, Client neu bauen.

Implementation-Notes (kein Architektur-Trade-off, für solcom-to-plan):

  • G5 — GDPR-Purge-Sequenz: FileUsageInterface::delete() (deregister) → $file->delete()$submission->delete(); Cascade-Verhalten von hook_webform_submission_delete prüfen, damit der eigene File-Cleanup nicht doppelt/zu spät läuft.
  • G6 — Retry-Tiefe: ADR-0050 setzt 5 Backoff-Stufen, der SfClient-Präzedenz (ADR-0033) nutzt 3 — bewusste Abweichung (längeres Retry-Fenster für Apply-Flow, ~6 h), im Plan kurz begründen statt stillschweigend abweichen.
  • G7 — Apply-CTA-Rewiring (ADR-0014): CTA hängt per Canvas-Config an field_apply_url.uri; Pfad A braucht die interne Sub-Route — als eigener Plan-Slice + QA-Test ausmodellieren, nicht implizit lassen.
  • G8 — E15 PII-Downstream: Drupal→Personio→Salesforce-Bewerber-PII (IT-Landscape 0001, E15, „PII-Klassifikation?" offen) — in keinem ADR/PRD adressiert; mit HR/Legal im AVV-Kontext (AK-17) mitklären.
  • G9 — PRD-Status-Drift: Out-of-Scope-Klausel „Bewerbungs-Formular auf Drupal — nur externer Link" im PRD 2026-05-02-personio-vacancy-integration-prd.html ist nie als superseded markiert; Strike-through/Notiz nachziehen.
  • G10 — AVV-Snapshot fehlt: AK-17 setzt einen AVV-Context-Snapshot unter docs/context/ voraus — existiert noch nicht; via solcom-docs-ingestion schließen (Implementations-Vorbedingung, kein Blocker).
2026-05-29 · Re-Grill Pivot C+ → A · solcom-grillMarc Philipps

Multiposting-Scope gewährt → Architektur auf Pfad A umgestellt (ADR-0050 supersedes ADR-0047). Re-Grilling-Session mit 7 Achsen-Entscheidungen, nachdem die C+-Implementierung im Review (2026-05-28) mit Acceptance:FAIL durchfiel. Entschieden: (1) Pfad A direkter API-POST; (2) async via Core-Queue + PersonioApplicationClient (analog SF-SfClient); (3) State als skalare Webform-Elemente statt _meta-Array (fix S-6); (4) Backoff via DelayedRequeueException, retry_count durable (fix S-5, Core-verifiziert); (5) Idempotency-Key aus recomputetem tracking_hash (neues Pfad-A-Risiko: Duplikate bei Retry); (6) status-differenzierte Retries (4xx → sofort DLQ, 429 → Retry-After, 5xx → Backoff); (7) DLQ-UI via webform_views + VBO (fix S-7). GDPR-Purge bei API-2xx via $submission->delete() + file_usage-Cleanup + Watchdog-Audit-Log (fix S-12). Webhook-ACK-Layer (AK-12/13/18 + 24h-Sweep) ersatzlos gestrichen. customer-lens-reviewer: GO-WITH-CONCERN (4 Concerns in ADR-0050 als C-1…C-4 eingearbeitet). Offene Probes: API-Request-/Response-Schema (multipart? 201+id? Idempotency-Key-Support?), Field-Parität.

2026-05-25 · Pivot-Decision · grill-with-docsMarc Philipps

Nach Spike-Result wird Architektur final auf Pfad C+ (Email-Inbox-Import mit Webhook-ACK) fixiert. MVP-Launch geht jetzt mit dieser Topology — Multiposting-Klärung mit Personio läuft parallel und nicht-blockierend. Falls Personio den Scope nachträglich aktiviert, wird der QueueWorker-Submission-Handler in einer separaten Story von SMTP auf HTTP-POST umgestellt (1-PR-Refactor, kein Architektur-Bruch). Story-Status wechselt von BLOCKED auf REFINED, Pfad-spezifische AKs werden zu C+-only konsolidiert.

2026-05-25 · Spike + Grilling · grill-with-docsMarc Philipps

Grilling-Session mit 9 + 2 Achsen-Entscheidungen. Q1 Webform-Topologie (geteilte Form + Kontext-Injection), Q2 Personio-API-Surface (initial A direkt, später durch Spike-Result revidiert), Q3 File-Upload-Profil (Standard + Pipe-Through, durch Queue-Wunsch auf Private-FS-temp revidiert), Q4 DSGVO Minimal-Konform, Q5 Sub-Route, Q-Queue Core-Queue + Custom-Retry, Q6 HR-Notification + Conditional-Retention, Q7 Honeypot only, Q8 Mock + Smoke gegen Dummy-Stelle, Q9 Field-Mapping-Hybrid. 17 Live-Probes gegen Personio-API: v1-Application-POST-Endpoint ist technisch live, aber für Standard-Recruiting-Credentials nicht erreichbar (Multiposting-Partner-Scope-Gate). v2-Recruiting ist vollständig read-only.

2026-05-11solcom-docs-explorer

Story-Hub initial angelegt mit User-Story und Hinweis „Kein Drupal-Bewerbungsformular" (F-100-Original-Scope: Redirect auf Personio-Hosted-Page). Diese Annahme wurde am 2026-05-25 durch die User-Direktive überschrieben.

Nächste Schritte

  1. ✓ Erledigt: ADR docs/adr/0050-vacancy-application-direct-personio-api-post.html (supersedes 0047) geschrieben, LOG + INDEX gepflegt. customer-lens-reviewer-Decision-Gate gelaufen → GO-WITH-CONCERN, 4 Concerns als C-1…C-4 in die ADR eingearbeitet (gemäß Memory feedback_customer_lens_before_adr_lock).
  2. Implementation-Plan unter docs/plans/ via solcom-to-plan-Skill auf Pfad-A-Slices aktualisieren: (1) Webform-Entity + Sub-Route + Vacancy-Context-Injection (UI-only), (2) Submit-Handler + Queue + private://-Storage + skalare State-Elemente, (3) QueueWorker + PersonioApplicationClient (POST) + Idempotency-Key + Failure-Klassifizierung via DelayedRequeueException, (4) GDPR-Purge bei HTTP-2xx + DLQ-HR-Notification, (5) DLQ-UI via webform_views + VBO, (6) Smoke-Test-Drush-Command + Kernel- & Functional-Tests.
  3. Jira-AK-Rewrite (User-Approval pro Ticket, kein autonomes Posten gemäß Memory feedback_no_autonomous_jira_comments) — Drafts liegen der Session bei:
    • P2503-90 (Story, In Arbeit) — Description auf Pfad-A-Outcome umschreiben.
    • P2503-184 (In Arbeit) — „Mail-Send" → „API-POST"; Credential-Setup (key-Modul) hier aufnehmen.
    • P2503-186 (In Arbeit) — von „Webhook-ACK schließt den Loop" auf „Idempotenz + GDPR-Purge bei API-2xx" repurposen (Webhook-Layer entfällt).
    • P2503-187 (Zu erledigen) — DLQ-Mechanik auf durable retry_count + status-differenziert + webform_views/VBO.
    • P2503-188 (Zu erledigen) — Smoke-Test auf Submit → Queue → POST → GDPR-Purge, Kernel-Tests auf PersonioApplicationClient (kein Webhook-Signature/Mail-Builder mehr; API-Read-back ist nicht verfügbar).
    • Unverändert: P2503-185 (Duplikat-Leiche, ignoriert). Erledigt: P2503-189 (Tenant-Config — Inbox-Teil gegenstandslos, Custom-Apply-URL bleibt gültig), P2503-190 (AVV), P2503-191 (Multiposting-Scope = Pivot-Unblocker).
  4. Wiki-Promote: neue Entity docs/wiki/vacancy-personio-integration.html als aggregierter Anker für Vacancy-Mirror (ADR-0035) + Apply-Flow (P2503-90/ADR-0050) + IT-Landscape-Edge E15 + Spike-Result. Erfüllt das 3-Strand-Threshold-Kriterium aus der html-doc-authoring-Skill-Konvention.

QA-Plan

Click-Pfade pro Akzeptanzkriterium — Quelle der Wahrheit für die Pre-PR-Acceptance-Stage des Ralph-Loops und für die Mensch-QA nach PR-Open. Source-of-truth für das Format: ADR-0037.

AK-#URL / PfadActionExpected
AK-1https://solcom.ddev.site/karriere/junior-sales-manager-vertrieb-2635453Vacancy-Detail-Seite öffnen, Apply-CTA visuell prüfen, ClickApply-CTA (Button oder sticky-apply-bar) sichtbar, Click navigiert auf /karriere/junior-sales-manager-vertrieb-2635453/bewerben (interne Sub-Route, kein Personio-Redirect)
AK-2/karriere/junior-sales-manager-vertrieb-2635453/bewerbenBewerbungsseite öffnen, Header und Form-Styling prüfenStellentitel sichtbar im Header, Webform vacancy_application rendert in byte_theme-Branding (Lime-Akzent, Montserrat-Headlines, ScalaSans-Body)
AK-3/karriere/.../bewerben vs. https://karriere.solcom.de/de/jetzt-bewerben/bewerbung_2602671-1Felder-Inventur visuell vergleichen (Side-by-Side via agent-browser)Identische Felder, identische Pflicht-Markierungen, identische Reihenfolge gegenüber Referenz-Form
AK-4/karriere/.../bewerben(a) PDF mit 11 MB hochladen — (b) JPG hochladen — (c) PDF mit 5 MB hochladen — (d) Submit ohne CV(a) Validation-Error Filesize — (b) Validation-Error Filetype — (c) Upload akzeptiert — (d) Submit-Block mit CV-Pflicht-Hint
AK-5/karriere/.../bewerbenDatenschutz-Checkbox-Wording und Link visuell prüfen, Submit ohne Checkbox versuchenWording und Layout 1:1 zur Referenz-Form, Link führt auf /datenschutz, Submit ohne Checkbox wird durch Webform-Validation blockiert
AK-6/karriere/.../bewerben + ddev drush queue:list + ddev exec ls private://applications/Gültige Submission abgeben mit Test-CVNavigation auf /karriere/.../bewerben/danke mit expliziter Erwähnung der folgenden Personio-Bestätigungsmail; Queue-Item in solcom_personio_application_queue sichtbar (Payload nur {submission_id}); Files-Verzeichnis private://applications/<submission-uuid>/ existiert mit hochgeladenen Dateien
AK-7/karriere/.../bewerben (DevTools)Honeypot-Hidden-Field via DevTools-Console füllen, Submit innerhalb 3 SekundenHTTP 200, Form schein-erfolgreich, aber keine Submission persistiert, kein Queue-Item erzeugt
AK-8ddev drush queue:run solcom_personio_application_queue (gegen Personio-Sandbox/Mock-Mode) + DB-/Watchdog-PrüfungWorker-Lauf manuell triggern mit einer pending SubmissionClient lädt vorhandene Files via POST /v1/recruiting/applications/documents hoch und setzt danach POST /v1/recruiting/applications als JSON ab; bei 2xx wird eine optional gelieferte personio_application_id auditierbar gemacht und die Submission GDPR-gepurged; Queue-Item ist verschwunden
AK-9ddev drush queue:run solcom_personio_application_queue + DB-Prüfung retry_count/status + ddev drush queue:listMock-Endpoint nacheinander auf (a) 500 (b) 429 mit Retry-After: 120 (c) 400 stellen, Worker je triggern(a) Item bleibt mit erhöhtem retry_count in Queue, nächster Lauf erst nach Backoff-Delay (DelayedRequeueException, kein Sofort-Re-Run) — (b) Item-Re-Run respektiert 120 s — (c) Item sofort dlq_persistent, retry_count nicht weiter erhöht, kein Retry-Burn
AK-10ddev drush queue:run 2× auf dieselbe Submission (Timeout-Simulation) + HTTP-Request-InspectionErsten POST künstlich timeouten lassen (Response verworfen), Item bleibt in Queue, zweiten Lauf auslösenBeide POSTs tragen identischen Idempotency-Key (= tracking_hash); Personio-Dedup muss separat mit Recruiting geklärt werden, weil kein API-Read-back verfügbar ist
AK-11Test-Mailbox hr@solcom.test + DB-Prüfung Submission-Status(a) Nach 5×-5xx-Erschöpfung — (b) nach sofort-4xx Test-Inbox und Submission-State prüfenIn beiden Fällen HR-Notification-Mail mit Submission-Details + Files-Pfad eingegangen; Submission-Status dlq_persistent, Files in private://applications/<uuid>/ bleiben bestehen
AK-12DLQ-View (Pfad aus webform_views-Config, z. B. /admin/karriere/dlq)(a) Als User mit HR-Rolle öffnen — (b) Als anonymer User öffnen — (c) Re-Queue-VBO-Action auf failed-Submission auslösen(a) View listet dlq_persistent-Submissions mit File-Download-Feld + VBO-Bulk-Actions — (b) HTTP 403 — (c) Action läuft über POST + CSRF (kein GET-Side-effect), Submission wandert zurück in Queue, Status zurück auf pending
AK-13DB-Prüfung Submission-Storage + ddev exec ls private://applications/<uuid>/ + ddev drush watchdog:show --type=solcom_vacancy_application nach 2xxSubmission nach erfolgreichem POST (2xx) prüfenSubmission-Entity gelöscht, Files-Verzeichnis entfernt (file_usage-Cleanup), Watchdog-Eintrag vorhanden und PII-frei (nur tracking_hash + application_id + Timestamp, kein Name/Email/Anhang)
AK-14ddev drush key:list + grep -r personio config/sync/(a) Key-Existenz prüfen — (b) config/sync nach Klartext-Credentials durchsuchen(a) solcom_personio_api_key mit Provider env vorhanden und personio_company_id in Modul-Config gesetzt — (b) kein Treffer mit Klartext-Credential in config/sync/
AK-15ddev exec ./vendor/bin/phpunit -c phpunit.xml web/modules/custom/solcom_vacancy_application/tests/Test-Suite laufenAlle Kernel-Tests grün (PersonioApplicationClient inkl. Field-Mapping, Idempotency-Key-Bildung, Status-Klassifizierung gegen Mock-HTTP-Client; File-Handling; GDPR-Purge); alle Functional-Tests grün (VacancyApplicationFormController, DLQ-View-Access 403). Kein Live-API-Call im Test-Lauf
AK-16ddev drush solcom:personio:smoke-testCommand gegen Dummy-Stelle 2635453 laufen lassen, anschließend im Personio-Recruiting-Backend prüfenCommand meldet erfolgreichen Submit → Queue → POST mit 2xx und Drupal-GDPR-Purge; Test-Application mit Email test+<timestamp>@solcom.de ist im Personio-Recruiting-Backend zu prüfen und dort zu löschen
AK-17docs/context/ (AVV-Snapshot-Pfad zu definieren via solcom-docs-ingestion)HR/Legal-Fakten-Check vor Implementation-StartAVV-Dokument liegt versioniert in docs/context/, im Story-Hub als Dependency referenziert, Status bestätigt durch HR/Legal-Reviewer