Personio-REST-Import: authentifizierte Spiegelung der Stellenanzeigen · superseded
node/vacancy. Auth via JWT-Bearer (Client-ID/Secret aus dem drupal/key-Modul mit Env-Provider). Sync läuft stündlich via hook_cron: synchroner Hard-Delete-Sweep, danach Queue-Item pro offene Stelle, @QueueWorker arbeitet Insert/Update ab. Datenmodell, Vocabularies und Canvas-Templates bleiben unverändert. Scope: Inbound-Sync der offenen Stellen + Compliance-konformer Auth-Layer + operative Tooling. Out of Scope: Bidirektionaler Sync, Bewerber-Daten, Real-time-Webhooks. Status: draft · Erstellt: 2026-05-15 · Story: P2503-106 (F-280) · MVP-Termin: 2026-07-08
Problem Statement
SOLCOM pflegt offene Stellenanzeigen vollständig in Personio (HR-Tooling). Die SOLCOM-Karriere-Section auf solcom.de spiegelt diese Stellen automatisiert in den Drupal-Bundle node/vacancy. Compliance- und IT-Security-seitige Anforderungen verlangen einen authentifizierten Zugriff auf Personio-Daten — anonyme oder öffentliche Datenquellen sind nicht mehr zulässig.
Stimme aus Compliance / IT-Security
„Externe Datenquellen werden authentifiziert angesprochen, mit nachvollziehbarem Audit-Trail. Credentials liegen nicht im Repository, Rotation muss möglich sein, und der Zugriff ist auf eine dedizierte Service-Identity gebunden. Personio liefert dafür eine REST-API mit Client-ID/Client-Secret-Authentication und JWT-Bearer-Tokens."
Solution
Das Modul solcom_vacancy erhält eine REST-API-basierte Sync-Architektur in drei Service-Schichten, getriggert von Drupal-Core-Cron und ausgeführt über die Drupal Queue-API. Bundle, Felder, Vocabularies und Canvas-Templates bleiben unverändert — der Sync ist transparent für die Anzeige-Schicht.
- Auth-Layer: Service
PersonioApiClientholt einen JWT-Bearer-Token vonPOST https://api.personio.de/v1/auth(Client-ID + Client-Secret aus demdrupal/key-Modul, das via Env-Provider aufPERSONIO_CLIENT_IDundPERSONIO_CLIENT_SECRETzugreift). Token wird incache.defaultmit TTLjwt.exp - 60sgehalten und bei 401-Responses einmal automatisch erneuert. - Sync-Orchestrierung:
hook_cron(stündlich, State-gated) ruft Personios Positions-Endpoint, vergleicht die zurückgegebenen Personio-IDs synchron gegen den Drupal-Bestand und löscht orphans bevor Queue-Items angelegt werden. Die noch vorhandenen Positions werden als Queue-Items inpersonio_position_importgelegt;@QueueWorkerarbeitet sie asynchron ab. - Field-Mapping: Service
PersonioPositionMapperübersetzt eine Personio-Position-Response in Drupal-Field-Werte, inklusive Auto-Create von Taxonomy-Terms bei Picklist-Drift.
Goals (V1)
- Service
PersonioApiClientauthentifiziert sich gegen Personio REST API v1, holt einen JWT-Bearer-Token, cached ihn mit TTL und erneuert bei 401 einmalig. - Credentials liegen nicht im Repo: Client-ID und Client-Secret werden als Key-Entities im
drupal/key-Modul angelegt, Storage über den Env-Provider (Env-VarsPERSONIO_CLIENT_ID+PERSONIO_CLIENT_SECRET). - Service
PersonioPositionMapperübersetzt eine Personio-Position-Response in das bestehende Vacancy-Field-Set; Term-Auto-Create-Logik für Picklist-Drift bleibt erhalten und loggt jeden Auto-Create als Watchdog-Warning. - Service
PersonioVacancySyncerorchestriert Pull → synchroner Sweep → Queue-Befüllung;@QueueWorkerarbeitet Items asynchron ab.hook_crontriggert stündlich (State-gated, identisch zur bisherigen Cadence). - Hard-Delete-Lifecycle: In Personio entfernte Stellen verschwinden aus Drupal nach maximal einer Cron-Periode. Empty-Response-Schutz: Bei 0 zurückgegebenen Positions wird der Sweep übersprungen und eine Watchdog-Warning geloggt, bestehende Nodes bleiben.
- Drush-Command
solcom_vacancy:syncerlaubt Manual-Run mit Status-Output (Anzahl synced/skipped/deleted, JWT-Token-Cache-State, letzter Cron-Run); ersetzt operativ den entfallendendrush migrate:status-Workflow. - Watchdog-Channel
solcom_vacancyloggt jeden Pull-Run (Anzahl Positions, Sweep-Aktionen, HTTP-Fehler, Token-Refreshes, Term-Auto-Creates). - Deployment-Sequence: Der atomare Merge von Sub-Task 4+5 (siehe Sub-Task-Plan) ersetzt den bestehenden Cron-Pfad durch den neuen — kein Zeitfenster mit konkurrierenden Sync-Mechanismen.
- ADR-0031 (key-Modul-Pattern für externe API-Credentials) und ADR-0032 (Service+Queue-Pattern für authentifizierte externe REST-Syncs ohne Migrate-API) dokumentieren die Pattern-Entscheidungen; ADR-0029 wird in-place auf das key-Modul-Pattern aktualisiert.
- Tests:
PersonioPositionMapper(Unit, ≥6 Test-Cases),PersonioApiClient+PersonioVacancySyncer+@QueueWorker(Kernel mit Mock-HTTP), JSON-LD-Rendering (Functional, unverändert vom Bestand).
User Stories
- Als HR-Verantwortliche:r möchte ich eine Stelle in Personio anlegen, ändern oder schließen, damit sie spätestens nach 60 Minuten auf solcom.de erscheint, aktualisiert wird oder verschwindet — ohne dass irgendwer manuell in Drupal etwas pflegt.
- Als Stellensuchende:r möchte ich offene SOLCOM-Stellen im Karriere-Bereich von solcom.de browsen, damit ich tagesaktuell den Bestand sehe.
- Als Stellensuchende:r möchte ich pro Stelle Office, Department, Schedule, Seniority und Gehalts-Range als deutsche Strings sehen, damit ich ohne HR-Vokabular sofort einordnen kann, worum es geht.
- Als Drupal-Editor:in möchte ich Vacancy-Nodes nicht manuell editieren oder anlegen können — Personio ist Source-of-Truth, jede Drupal-seitige Änderung würde beim nächsten Sync überschrieben (konsistent mit ADR-0026 Vacancy-Editorial-Ausschluss).
- Als System-Operator:in möchte ich, dass Personio-API-Credentials nicht im Git-Repository liegen, damit der Sicherheits-Standard eingehalten wird und Rotation möglich ist.
- Als System-Operator:in möchte ich, dass der Sweep nicht den gesamten Vacancy-Bestand löscht, wenn die Personio-API einen leeren Response liefert, damit Personio-Outages nicht zu Daten-Verlust führen.
- Als System-Operator:in möchte ich einen Drush-Command für Manual-Run und einen Status-Output, damit ich ohne Browser-Login den Sync-Zustand bewerten kann.
- Als Compliance-Verantwortliche:r möchte ich, dass jeder Personio-Zugriff authentifiziert läuft (Service-Identity, kein anonymes Lesen), damit der Audit-Trail vollständig ist.
- Als Developer:in möchte ich, dass auto-erzeugte Taxonomy-Terms (bei Personio-Picklist-Drift, z. B. ein neues Office oder Department) mit einer Watchdog-Warning auffallen, damit Editor:innen die deutschen Labels proaktiv nachpflegen können.
- Als Developer:in möchte ich, dass alle Personio-Response-Annahmen vor der ersten Mapper-Zeile gegen die echte API verifiziert werden (Spike-Pre-Condition), damit Field-Name-Drifts nicht erst im Kernel-Test auffallen.
- Als Developer:in möchte ich, dass das Pattern für authentifizierte externe REST-Syncs (Auth via key-Modul, Service+Queue ohne Migrate-API) explizit als ADR dokumentiert ist, damit künftige Integrationen (Salesforce-Publication-Sync, weitere) konsistent gebaut werden.
Implementation Decisions
Sync-Stack — Service + hook_cron + Queue-API (kein Migrate-API)
Der Sync wird als Service-Layer (PersonioApiClient + PersonioPositionMapper + PersonioVacancySyncer) implementiert, der von hook_cron getriggert wird. Idempotenz läuft über das bestehende Feld field_personio_id (Drupal-EntityQuery-Lookup vor Insert/Update). Hard-Delete-Sweep läuft synchron im hook_cron, bevor Queue-Items angelegt werden. Asynchrone Insert/Update-Verarbeitung via @QueueWorker.
Rejected: Migrate-API mit migrate_plus + Custom-Authentication-Plugin — Migrate-APIs Source-Plugin-Ökosystem ist auf öffentliche oder einfach-authentifizierte Feeds optimiert; ein JWT-Bearer-Flow mit Token-Cache und Refresh-on-401 zwingt ohnehin in Custom-Code (Custom Fetcher oder Custom Authentication-Plugin). Damit verliert die Migrate-API ihren Mehrwert (ID-Map, Process-Pipeline) gegenüber einem eigenen Service-Layer, behält aber den Maintenance-Surface. Service+Queue ist ehrlicher und kleiner. Konsequenz: migrate_plus und migrate_tools entfallen als Modul-Dependencies. Begründung als ADR-0032 dokumentiert. Konsequenz für CONTEXT.md: Glossar-Eintrag „Vacancy — pulled via drupal:migrate" wird auf das neue Pattern angepasst.
Rejected: Komplett-Eigenbau ohne Drupal-Queue-API (synchrone Verarbeitung im hook_cron) — funktioniert technisch bei < 100 Stellen, hat aber keinen Headroom für Wachstum oder Personio-Latenz und verzichtet auf Retry-Backoff bei transienten Fehlern. Queue-API addet wenig Code (~30 LoC für QueueWorker-Annotation und Worker-Body), bringt Drupal-native Retry-Mechanik mit.
API-Client + Auth-Layer
Service PersonioApiClient in src/Service/PersonioApiClient.php. Verantwortlich für:
- Token-Fetch:
POST https://api.personio.de/v1/authmit JSON-Body{"client_id": "...", "client_secret": "..."}liefert einen JWT-Bearer-Token. - Token-Cache: JWT wird in
cache.defaultpersistiert, TTL =jwt.exp - 60s(1 Minute Safety-Buffer gegen Clock-Skew). - Request-Wrapper: generische
request(string $method, string $path, array $options = []): ResponseInterface, fügtAuthorization: Bearer ...automatisch hinzu, erneuert Token bei 401 einmalig und führt einen Retry durch. - Logging: alle Auth-Fehlversuche, Token-Refreshes und 5xx-Responses auf den Watchdog-Channel
solcom_vacancy.
Token-Service ist bewusst lokal im Modul solcom_vacancy — kein generisches solcom_integrations-Modul. Falls SF-Publication beim Bau dasselbe Pattern adoptiert, ist die Extraktion in einen Shared-Layer ein additives Refactor — Karpathy-Prinzip: nicht für hypothetische Zukunft designen.
Credential-Storage — drupal/key-Modul mit Env-Provider
Modul drupal/key wird als Composer-Dependency hinzugefügt und im Default-Sync aktiviert. Zwei Key-Entities (personio_client_id, personio_client_secret) liegen als key.key.*.yml in config/sync/ — die Config-Files enthalten nur die Provider-Konfiguration, nicht die Werte. Provider: env (key-Modul Built-in). Reale Werte werden via Env-Vars PERSONIO_CLIENT_ID und PERSONIO_CLIENT_SECRET in settings.local.php (lokal) und Mittwald-App-Config (Stage/Prod) bereitgestellt.
ADR-0029 wird in-place aktualisiert: das dort definierte SF-Pattern (direktes getenv() im hook_migration_plugins_alter) wird durch das key-Modul-Pattern ersetzt. SF-Publication adoptiert das key-Modul-Pattern beim Bau (P2503-108). Begründung als ADR-0031 dokumentiert.
Rejected: Direktes getenv() in Service-Code (kein Audit-Trail in Drupal, keine UI für Rotation, keine Provider-Abstraktion); drupal/encrypt mit Drupal-DB-Schlüssel (Schlüsselverwaltung wird selbst-referenziell); File-Provider von drupal/key mit auf-dem-Server-File (kein operativer Vorteil gegenüber Env-Var, dafür zusätzlicher File-Permission-Surface).
Position-Mapper — JSON zu Drupal-Field-Werten
Service PersonioPositionMapper in src/Service/PersonioPositionMapper.php. Methode map(array $position): array liefert das Drupal-Field-Array für den Bundle vacancy. Verantwortlich für:
- Field-Translation: Personio-JSON-Pfade → Drupal-Field-Werte (z. B.
office→field_office-Term-Lookup,salary.min/salary.max/salary.currency→field_salary_*). - Description-Aggregation: Personios strukturierte Job-Sections (z. B. „Aufgaben", „Anforderungen") werden zu einem HTML-Body konkateniert; Sections wie „Team" / „Ansprechpartner/in" werden gemäß bestehender Konvention ausgeblendet (carry-over aus dem bisherigen Datenkanal). Konkrete Section-Struktur klärt der Spike (Sub-Task 1).
- Term-Auto-Create: Vocabularies (
vacancy_office,vacancy_department,vacancy_keyword,vacancy_recruiting_category,vacancy_subcompany,vacancy_employment_type,vacancy_seniority,vacancy_schedule,vacancy_years_experience,vacancy_occupation_category) bekommen automatisch neue Terms bei Picklist-Drift; jeder Auto-Create wird auf den Watchdog-Channelsolcom_vacancymit Severitywarninggeloggt. - Apply-URL-Strategie: Personios Response-Schema klärt der Spike (Sub-Task 1). Wenn die Response ein
apply_url-Feld liefert, wird das übernommen; falls nicht, wird die URL ausapply_base + position.idkonstruiert (carry-over der bestehenden Konventionhttps://solcom-gmbh.jobs.personio.de/job/<id>). - Salary-Display: Computed-Field
field_salary_displaybleibt unverändert (Code-PfadSalaryDisplayComputedItemList).
Cron + Queue + Sweep
hook_cron in solcom_vacancy.module ruft PersonioVacancySyncer::syncFromCron(). State-Gating bleibt erhalten (kein zweiter Run innerhalb von 3600s). Lifecycle pro Run:
- Pull:
GET https://api.personio.de/v1/recruiting/positionsliefert die Liste der offenen Stellen. - Sweep (synchron, race-frei): Drupal-EntityQuery: Vacancy-Nodes mit
field_personio_id NOT IN $current_personio_idswerden direkt viaEntityStorage::delete()entfernt. Empty-Response-Schutz: Bei 0 Personio-IDs wird der Sweep übersprungen und eine Watchdog-Warning geloggt — bestehende Nodes bleiben. - Enqueue: Für jede in der Response enthaltene Position wird ein Queue-Item
{personio_id: "<id>", payload: <array>}inqueue.factory.get('personio_position_import')gelegt. - Verarbeitung:
@QueueWorker('personio_position_import')arbeitet jedes Item ab — Lookup-or-Create Drupal-Node anhandfield_personio_id, Mapping viaPersonioPositionMapper, save.
Die Annotation cron => ['time' => 60] auf dem QueueWorker erlaubt dem Drupal-Cron pro Cron-Run maximal 60 Sekunden auf der Queue zu arbeiten — ausreichend für SOLCOMs Stellen-Bestand (geschätzt 50-100 Stellen) mit Headroom für Personio-Latenz und Retry-Backoff bei transienten 5xx-Fehlern.
Operative Sichtbarkeit
Da Migrate-Tools entfällt, brauchen wir expliziten Tooling-Ersatz:
- Drush-Command
solcom_vacancy:sync(Manual-Run): triggertPersonioVacancySyncer::syncFromCron()synchron und bearbeitet die Queue komplett ab, gibt strukturiertes Result (anzahl_synced, anzahl_deleted, anzahl_errors, last_token_refresh, queue_depth) zurück. - Drush-Command
solcom_vacancy:status(Read-only): letzter erfolgreicher Pull-Run-Timestamp, Token-Cache-State, Queue-Depth, Anzahl Vacancy-Nodes. - Watchdog-Channel
solcom_vacancy(insolcom_vacancy.services.yml): jeder Pull-Run loggt Anzahl Positions, Sweep-Aktionen, HTTP-Fehler, Token-Refreshes, Term-Auto-Creates. - State-Tracking:
solcom_vacancy.last_sync(Timestamp) undsolcom_vacancy.last_sync_result(Result-Array) inDrupal::state().
Modul-Layout solcom_vacancy (Soll-Zustand)
web/modules/custom/solcom_vacancy/
├── solcom_vacancy.info.yml # dependencies: drupal/key, drupal/node, drupal/taxonomy, ... (kein migrate_plus, kein migrate_tools)
├── solcom_vacancy.install
├── solcom_vacancy.module # hook_cron → PersonioVacancySyncer; hook_node_view → JSON-LD (unverändert)
├── solcom_vacancy.permissions.yml
├── solcom_vacancy.services.yml # PersonioApiClient, PersonioPositionMapper,
│ # PersonioVacancySyncer, VacancyJsonLdBuilder,
│ # logger.channel.solcom_vacancy
├── src/
│ ├── Service/
│ │ ├── PersonioApiClient.php
│ │ ├── PersonioPositionMapper.php
│ │ ├── PersonioVacancySyncer.php
│ │ └── VacancyJsonLdBuilder.php # unverändert
│ ├── Plugin/
│ │ └── QueueWorker/
│ │ └── PersonioPositionWorker.php
│ ├── Commands/
│ │ └── SolcomVacancyCommands.php # drush commands: sync, status
│ └── SalaryDisplayComputedItemList.php # unverändert
└── tests/src/
├── Unit/Service/
│ └── PersonioPositionMapperTest.php
└── Kernel/
├── Service/
│ ├── PersonioApiClientTest.php
│ └── PersonioVacancySyncerTest.php
└── Plugin/QueueWorker/
└── PersonioPositionWorkerTest.php
Zusätzlich in config/sync/: key.key.personio_client_id.yml, key.key.personio_client_secret.yml, Update von core.extension.yml (key aktiviert, migrate_plus + migrate_tools entfernt). Die bestehenden Migrate-YAMLs migrate_plus.migration.solcom_vacancy.yml und migrate_plus.migration_group.solcom_vacancy.yml entfallen.
Sub-Task-Plan — 6 Vertical Slices
- Spike + ADR-Trio: Live-Spike gegen Personio REST API v1 — echter Token-Fetch, echter Positions-Call, Field-Mapping-Annahmen (apply_url, description-struktur, paging) bestätigen. Anlegen von ADR-0031 (key-Modul-Pattern) und ADR-0032 (Service+Queue-Pattern); in-place Update von ADR-0029. Spike-Findings als Open-Items-Resolutions zurück ins PRD.
- PersonioApiClient + key-Modul:
drupal/keyals Composer-Dep hinzufügen + im Default-Sync aktivieren, zwei Key-Entities mit Env-Provider,PersonioApiClient-Service mit Token-Cache und Refresh-on-401. Unit-Tests für Token-Cache-Logik, Kernel-Tests mit Mock-HTTP. - PersonioPositionMapper: JSON-Position-Object → Drupal-Field-Array, inkl. Term-Auto-Create und Description-Aggregation. Unit-Tests gegen Fixture-JSON-Responses (≥6 Cases: happy path, fehlende optional fields, mehrere Job-Sections, leere Salary-Range, neue Office-Picklist, neue Department-Picklist).
- PersonioVacancySyncer + QueueWorker + hook_cron: Sync-Orchestrierung mit synchronem Sweep, Queue-Befüllung, Worker-Verarbeitung. Kernel-Tests gegen echte Drupal-DB + Mock-HTTP (Insert; Update; Sweep mit 3 Test-Nodes; Empty-Response-Schutz).
- Cleanup Migrate-Legacy:
migrate_plus/migrate_toolsentfernen (Composer + info.yml + config/sync/-YAMLs), alte Migrate-Plugin-Files untersrc/Plugin/migrate/löschen,VacancyHardDeleteSubscriberentfernen, altersolcom_vacancy_cron-Hook (MigrateExecutable-Pfad) durch den neuen ersetzen. CONTEXT.md-Glossar-Eintrag aktualisieren. - Operative Tooling: Drush-Commands
solcom_vacancy:sync+solcom_vacancy:status, Watchdog-Channel-Definition, State-Tracking. Optional: README im Modul-Root mit Operations-Cheatsheet.
Slices 2-4 sind nach Slice 1 parallelisierbar. Slice 5 ist zerstörerisch (Composer/Files/Config) und wird atomar mit Slice 4 gemerged — damit existiert kein Zeitfenster, in dem alter und neuer Sync-Pfad parallel laufen. Slice 6 läuft additiv zu Slice 4. Empfohlene Deployment-Reihenfolge: 1 → 2 → 3 → 4+5 (atomar) → 6.
Testing Decisions
Tests folgen der Pyramide: deterministische Logik isoliert (Unit), Drupal-Container-abhängige Services und Pipelines im Kernel-Test (Mock-HTTP via http_client_factory-Service-Override), JSON-LD-Rendering im Functional-Test (bleibt vom Bestand). Keine FunctionalJavascript-Tests — keine UI-Änderungen.
| Test | Typ | Coverage-Ziel |
|---|---|---|
PersonioPositionMapperTest | Unit | Happy Path; fehlende Felder; mehrere Job-Sections; leere Salary-Range; Picklist-Drift (Auto-Create); Section-Ausblendung |
PersonioApiClientTest | Kernel + Mock-HTTP | Token-Fetch; Token-Cache-Hit; Refresh bei 401; Logging-Verhalten; 5xx-Behandlung |
PersonioVacancySyncerTest | Kernel | Insert; Update; Sweep mit 3 Nodes; Empty-Response-Schutz; State-Tracking |
PersonioPositionWorkerTest | Kernel | Item-Verarbeitung; Mapper-Integration; Error-Handling (transienter 5xx → re-throw → Drupal-Retry) |
| JSON-LD-Rendering | Functional | Aus dem Bestand übernommen, unverändert |
Out of Scope
- Bidirektionaler Sync — keine Drupal-Daten zurück nach Personio. Personio bleibt Source-of-Truth, Drupal ist Read-Mirror.
- Bewerber-Daten / Recruiting-Workflow — wir lesen ausschließlich offene Stellen, keine Bewerbungen oder Kandidaten-Daten. Bewerbungen erfolgen weiterhin extern (Personio-Apply-URL).
- Real-time-Webhooks — Personio bietet Webhook-Subscriptions, aber wir bleiben bei stündlichem Pull (Cron-Pattern, vorhersehbar, einfach, kein Endpoint-Hosting nötig).
- Mehrere Personio-Mandanten — eine SOLCOM-Personio-Org, eine Service-Identity, eine Drupal-Site. Multi-Tenancy ist nicht Scope.
- Generische Integrations-Schicht — kein
solcom_integrations-Modul. Wenn SF-Publication beim Bau das Pattern adoptiert, wird der gemeinsame Layer dort additiv extrahiert. - Admin-UI für Sync-Status — keine Drupal-Admin-Routen, kein Block, keine Toolbar-Items. Drush-Commands reichen für V1.
Open Items
BLOCKER 2026-05-15: Live-Spike gegen Personio hat die Plan-Kern-Annahme „authentifizierte REST liefert vollständigen Content" widerlegt. PRD und Plan brauchen ein Reframe (Hybrid / XML-only / Pause). Details + Beispiel-Bodies + verifizierte Fakten siehe Story-Hub F-280, Section Spike-Findings.
- Architektur-Reframe (Blocker — Owner: Marc + SOLCOM-IT): Drei Optionen pending Stakeholder-Klärung — Hybrid (REST-Lifecycle + XML-Content), XML-only (Plan radikal vereinfachen), oder Pause/GA-warten. Bis Entscheidung gefallen ist, kann der Implementation-Plan nicht gestartet werden.
- Field-Mapping-Verifikation — RESOLVED: REST-Endpoint
/v1/recruiting/positionsexistiert nicht; korrekt istGET /v2/recruiting/jobsmit HeaderBeta: true. Pro Job nur 10 Header-Felder (id, name, category, company, department, hiring_team, created_at, updated_at, requirements_parsed_data, total_requirements) — die letzten zwei sind in allen 2345 Samplesnull. Keinapply_url, kein Bewerbungs-Link. Konstruktion viaapply_base + position.idbleibt einziger Weg. - Paging — RESOLVED: cursor-basiert,
?limit=200&cursor=…, Default 100. Termination muss über_data === []laufen, nicht über fehlendesnext— Personiosnext-Link kommt auch nach erschöpfter Page-Folge weiter, was zu Zirkular-Loops führt. Bei SOLCOMs aktueller Org-Größe: 429 distinct Jobs in REST verteilt auf ~3 Pages à 200. - Description-Struktur — RESOLVED: REST liefert keine Description, kein
jobDescriptions-Feld, keine Sections. Die{name, value}-Struktur (Sections: „Das erwartet Dich", „Das bringst Du mit", „Deine Benefits", „Standort", „Team", „Startdatum", „Ansprechpartner/in") existiert ausschließlich im public XML-Feedsolcom-gmbh.jobs.personio.de/xml. Der Plan-Annahme „Description-Aggregation aus REST-Sections" fehlt damit die Datenbasis. - Token-Format — RESOLVED:
papi-…-prefix, opaque (kein JWT, keineexp-Claim-Inspektion möglich). TTL kommt ausexpires_in-Feld der/v1/auth-Response (86400s = 24h). Plan-AK „JWT-Format dokumentiert" sowie Plan-Code-Snippet „JWT-Cache mit TTLjwt.exp - 60s" sind technisch nicht haltbar — Cache-TTL muss ausexpires_in - safety_marginberechnet werden. - Auth-Flow — RESOLVED:
POST https://api.personio.de/v1/authmitContent-Type: application/x-www-form-urlencodedund Bodyclient_id=…&client_secret=…. Response:{"success": true, "data": {"token": "papi-…", "expires_in": 86400, "scope": "personio:recruiting:read personio:recruiting:write personio:legal-entities:read personio:workplaces:read"}}. - SF-Adoption — pending (unverändert): ADR-0029-Update setzt das key-Modul-Pattern auch für SF-Publication. Wer baut die Migration der SF-Story (P2503-108) auf das key-Pattern um — als Sub-Task hier oder im SF-Bau-Sprint? Vorschlag: SF-Sprint, weil die SF-Story noch nicht in Arbeit ist und ein dortiger Adopt günstiger ist als ein hier-eingeschobener. Hängt vom Reframe-Outcome ab — falls XML-only gewählt wird, entfällt der gemeinsame Auth-Layer.
- Token-Cache-Bin — pending (unverändert):
cache.defaultreicht für 1 Token. Falls SF-Publication beim Bau dasselbe Cache-Bin nutzt, prüfen, ob ein dedicatedcache.solcom_integrations_tokensinnvoll wäre (Invalidation-Granularität). Hängt vom Reframe-Outcome ab.
FAQ
- Warum bauen wir das überhaupt jetzt?
- Compliance- und IT-Security-seitige Anforderung: externe Datenquellen müssen authentifiziert angesprochen werden, mit nachvollziehbarem Audit-Trail und Service-Identity. Personio liefert dafür Service-Identity-Credentials, die wir in Drupal einbauen.
- Warum kein Migrate-API?
- Migrate-APIs Source-Plugin-Stack ist auf öffentliche oder einfach-authentifizierte Feeds optimiert. Personios JWT-Bearer-Flow zwingt uns ohnehin in Custom-Code (Custom Fetcher oder Authentication-Plugin). Damit verliert Migrate den Mehrwert (ID-Map, Process-Pipeline) gegenüber einem eigenen Service-Layer — der ist kleiner und ehrlicher. Begründung in ADR-0032.
- Warum drupal/key statt direkt getenv()?
- Audit-Trail in Drupal, UI für Rotation, Provider-pluggable (Env / File / Secret-Manager). ADR-0029 hatte ursprünglich direktes
getenv()vorgesehen — das Pattern wird mit dieser Story konsolidiert und SF-Publication adoptiert es beim Bau. Begründung in ADR-0031. - Warum Queue-API bei nur ~50–100 offenen Stellen?
- Headroom für Wachstum und Personio-Latenz. Synchrone Verarbeitung würde funktionieren; mit Queue-API bleibt der Sync-Run pro Cron-Cycle zeitlich gedeckelt (Annotation
cron => ['time' => 60]), und wir haben Retry-Backoff bei transienten Fehlern „for free". - Wie wird die Deployment-Sequence garantiert?
- Die Sub-Tasks 4 und 5 werden atomar gemerged: derselbe Commit, der den neuen
hook_cron-Pfad aktiviert, entfernt auch den altenMigrateExecutable-Pfad. Damit ist kein Zeitfenster möglich, in dem beide Pulls parallel laufen. - Was passiert mit dem alten PRD 2026-05-02-personio-vacancy-integration-prd.html?
- Es wird in der Frontmatter dieses PRDs als
solcom:supersedes-prdreferenziert und im alten PRD wird der Status aufsupersededgesetzt mit Verweis auf dieses Dokument. Inhalte bleiben als historischer Kontext erhalten. - Was passiert mit den bestehenden Tests (z. B. VacancyDescriptionsConcatTest)?
- Die fallen mit den Migrate-Process-Plugins weg. Die Test-Substanz (Description-Concat-Logik) wird in den neuen
PersonioPositionMapperTestaufgenommen — die Test-Cases bleiben semantisch erhalten, nur die Test-Klasse und das System-Under-Test wechseln. - Was ist mit dem öffentlichen Personio-Feed unter
https://solcom-gmbh.jobs.personio.de/xml? - Dieser Feed ist ein Personio-seitiges Konstrukt für SEO und Career-Portal-Embeds und bleibt unabhängig vom Drupal-Sync existent. Drupal greift künftig nicht mehr darauf zu.