P2503 · Projektpartnerportal · F-290

Salesforce-Publication-Sync (BH4SF) — REST-Pull + Outbound-URL-Writeback

REFINED P2503-108 Erstellt: 2026-05-11 · Refined: 2026-05-13 · Frontend-Decomposition: 2026-05-14

User Story

Als Online-Redakteurin von SOLCOM möchte ich, dass alle in der zentralen Salesforce-Org (FCSB) zur Veröffentlichung freigegebenen Publication-Datensätze automatisch in Drupal als sf_publication-Nodes erscheinen und nach Unpublish in Salesforce wieder verschwinden, damit das Projektpartner-Portal jederzeit exakt den Veröffentlichungsstand aus Salesforce widerspiegelt und keine manuelle Doppelpflege zwischen CRM und Website nötig ist. Zusätzlich soll Drupal beim Publish die kanonische PPP-Detail-URL nach hohr__Job_Application_URL__c in Salesforce zurückschreiben, damit Berater:innen den freigegebenen Stand direkt aus dem CRM heraus aufrufen können.

Phase 2 (post-MVP): Freiberufler-/Berater-Account-Verknüpfung im PPP — User-Linking-Workflow bleibt im Scope der übergeordneten Initiative, ist aber nicht Teil dieser Story (F-400).

Technische Beschreibung

Backend Salesforce-FCSB-Org, BH4SF-Layer (Bullhorn-for-Salesforce). Drupal-Bundle sf_publication spiegelt TR1__Job__c-Records mit RecordTypeId = 012Wx000000C7ngIAC (DeveloperName Publication) stündlich via Migrate-API (REST-Pull, OAuth-2-Refresh-Token-Flow, JSON+SOQL+nextRecordsUrl-Paging). Hard-Delete bei SF-Verschwinden via Per-Run-Sweep im POST_IMPORT-EventSubscriber. Bei Drupal-Publish/Unpublish wird die kanonische PPP-URL nach hohr__Job_Application_URL__c via QueueWorker zurückgeschrieben.

Hinweise / Kommentare

Scope-Klärung (2026-05-13 Refinement): Diese Story fokussiert auf A — Publication-Pull + C — Outbound-URL-Writeback (Phase 1). B — User-Profile-Pull via field_sf_contact_id + SF-Contact-Spiegelung wird in eine separate Story für Phase 2 ausgegliedert (abhängig von PPP-Auth-Strategie, Story F-400). Out-of-Scope für V1: Merkliste, Bewerbungs-Formular (F-330 Minggo), Multi-Field-Outbound, SF-Push/Platform-Events, Bullhorn-Direct-Integration (alles via BH4SF auf SF).

Architektur folgt Repo-Standard ADR-0013 (inline im Personio-PRD): Migrate-API für externe Quellen, Custom-QueueWorker rejected. Editor-Boundary analog ADR-0026: SF-Publications sind in Drupal nicht editierbar.

Phase: Projektpartnerportal (Entscheidung aus Backlog-to-Jira-Sweep 2026-05-11 — überschreibt Excel-Phase „Schnittstellen").

Kontext aus docs/

  • PRD: 2026-05-13-salesforce-publication-sync-prd.html — 18 User-Stories, 8 Implementation-Decisions, Testing-Plan, Out-of-Scope, 4 Open-Items
  • ADR: ADR-0029 — Hybrid-Architektur (Migrate inbound + Custom outbound + Per-Run-Sweep), Status proposed
  • Mapping-Snapshot: Context-0008 FCSB · BH4SF Publication-Mapping — 22 Inbound-Felder, 1 Outbound-Feld, 25 Industry-Picklist-Werte mit DE-Labels, SOQL-Anker-Query, Field-Name-Drift-Warnungen
  • Personio-Präzedenz: Personio-Vacancy-Integration-PRD mit inline ADR-0012/0013 — Migrate-Pattern-Standard für externe Quellen, Hard-Delete-Sweep im Vacancy-Modul als 1:1-Vorlage
  • IT-Landschaft: Context-0001, Kante E11 (Salesforce-Job-Posting → Projektpartnerportal als „Existing Drupal Webhook")
  • Outbound-Abgrenzung: F-330 Minggoformular — Bewerbungs-Submit als iFrame-Embed, kein Drupal-Datendurchsatz, nicht in F-290
  • Phase-2-Anker: User-Linking via field_sf_contact_id hängt an F-400 (Drupal-native PPP-Auth) — eigene Story sobald Auth-Strategie steht

Akzeptanzkriterien

  1. Content-Type sf_publication in config/sync/ definiert (21 Felder gemäß Mapping aus Context-0008, Storage + Field-Config), drush cim-reproducible.
  2. Vocabulary sf_industry mit 25 Terms (DE-Labels + field_sf_value) per hook_install pre-seeded.
  3. Modul solcom_sf_publication im Default-Sync aktiviert.
  4. Migration sf_publication läuft stündlich (Cron 3600s), idempotent über SF-Id, mit nextRecordsUrl-Paging und RecordType-Filter 012Wx000000C7ngIAC.
  5. OAuth-2-Refresh-Token-Flow funktioniert gegen FCSB-Sandbox; Token wird automatisch refresht; Credentials liegen NICHT im Repo ($config-Override aus Env-Vars SF_FCSB_CLIENT_ID/_SECRET/_REFRESH_TOKEN).
  6. Hard-Delete-Lifecycle: SF-verschwundene Records → Drupal-Node-Delete nach max. 1 Stunde via MigrateEvents::POST_IMPORT-EventSubscriber. Empty-Feed-Schutz: 0 Records → Sweep skip + Watchdog-Warning. HTTP-Fehler → kein Sweep.
  7. Outbound-PATCH funktioniert: Publish-Transition → hohr__Job_Application_URL__c wird gesetzt; Unpublish-Transition → Feld wird geleert (null).
  8. Outbound-Triggers in zwei Hooks: hook_node_insert enqueued set wenn ein neu erstellter sf_publication-Node bereits published ist (Migrate-Initial-Pfad, $node->original ist null); hook_node_update mit Trigger-Guard $node->original->isPublished() !== $node->isPublished() deckt spätere Status-Transitionen ab und verhindert Queue-Flooding bei Migrate-internen Re-Saves.
  9. Industry-Lookup: Custom Process Plugin matcht SF-Value (z.B. "Banking") gegen field_sf_value, gibt Drupal-Term mit DE-Label ("Bankwesen") zurück. Auto-Create-on-Miss mit Watchdog-Warning.
  10. Rich-Text-Sanitize: 4 Felder (hohr__Long_description__c, hohr__Internal_description__c, hohr__Requirements_description__c, TR1__Experience_Requirements__c) durchlaufen SalesforceRichTextSanitizer-Service (DOMDocument + Whitelist). Force.com-<font>, MS-Word-<o:p>, inline-Styles entfernt vor DB-Write.
  11. Lifecycle-Mapping: Drupal-Node ist published gdw. SC_PublicationStatus__c == "Online" AND publish_start <= now < publish_end. Implementiert via static_map-Process-Plugin.
  12. Drupal-Editor:innen können sf_publication-Nodes nicht manuell editieren oder anlegen (Permissions strict).
  13. Tests: Unit (Sanitizer), Kernel (3 Process-Plugins + HardDeleteSubscriber), Mocked-HTTP (SalesforceClient), Functional (End-to-End mit Guzzle-MockHandler).

Abhängigkeiten

  • Composer: drupal/migrate_plus (bereits im Repo für solcom_vacancy), drupal/migrate_tools, drupal/key (Backup-Storage).
  • SF-Setup: Connected-App in FCSB-Org mit OAuth-Scopes api, refresh_token; Refresh-Token einmalig generiert.
  • ENV-Vars: SF_FCSB_CLIENT_ID, SF_FCSB_CLIENT_SECRET, SF_FCSB_REFRESH_TOKEN in lokal settings.local.php bzw. Mittwald-Staging/Live-Env.
  • Repo-Konventionen: ADR-0013 (Migrate-Pattern für externe Quellen), ADR-0026 (Editor-Boundary), ADR-0010 (config_split / dev-only-Module), ADR-0011 ($config-Override-Pattern für Tokens).
  • Hosting: Drupal-Cron muss laufen (Mittwald Cron-Job konfiguriert), Outbound-PATCH läuft als Queue-Tick.
  • Vorgänger-Story: Keine (greenfield Modul).
  • Folge-Story: F-290.B User-Profile-Pull + field_sf_contact_id (Phase 2, abhängig von F-400 PPP-Auth).

Frontend-Decomposition (2026-05-14)

Frontend-Slice unter P2503-108 wurde in der Refinement-Session 2026-05-14 in 14 granulare, agent-ready Sub-Tasks zerlegt. Quelle: Figma-Frames 1019-78888 (Detail) und 985-50077 (List) plus Vacancy-Präzedenz (canvas.content_template.node.vacancy.{card,full}.yml, views.view.vacancies_listing.yml, ADR-0014).

Layer A — MVP-Skeleton (Pflicht zum 08.07.2026)

  • F1 / P2503-137 — View-Mode-Config (default + card) + Pathauto-Pattern /projektpartnerportal/[node:title].
  • F2 / P2503-140 — Canvas-Komposition sf_publication.full (Detail-View, Slot-Sequenz aus bubble-profile + card-meta-list + meta-row + button + Cross-Sell-Slot + Sticky-CTA-Bar). Figma 1019-78888.
  • F3 / P2503-141 — Canvas-Card-Template sf_publication.card (Listing-Row, prop-driven mit card-meta-list). Figma 985-50077.
  • F4 / P2503-142 — Drupal-View publications_listing + BEF mit 5 Filter-Facetten (Branche, Einsatzort, Auslastung, Dauer, Wunschstart).
  • F5 / P2503-143 — Canvas-Page /projektpartnerportal (288px Sticky-Left-Filter-Sidebar + Views-Block-Embed). Figma 985-50077.
  • F6 / P2503-144 — Custom-Block-Plugin PPP-Cross-Sell (2 editor-pflegbare Card-Slots).
  • F7 / P2503-145 — Figma-MAPPING-Update + ADR-0030 sf-publication-frontend-decomposition.

Layer B — Figma-Vollparität (Post-MVP, weiterhin unter P2503-108)

  • F8 / P2503-146 — Pre-Build-Gate: Live-Describe FCSB-Sandbox auf Skills- und Remote-Anteil-Feld. Blockiert F9 + F10.
  • F9 / P2503-147 — Skill-Badges auf Card + Skills-Filter (abhängig von F8).
  • F10 / P2503-148 — Remote-Anteil-Slider in Card + Range-Filter (abhängig von F8).
  • F11 / P2503-149 — Suchradius-Filter (drupal/geofield + Geocoder + Distance-Sort).
  • F12 / P2503-150 — Merkliste (anon LocalStorage, max. 5 Projekte; Sticky-Off-Canvas-Bar List-Variante).
  • F13 / P2503-151 — Matching-Rate auf Card (V1 faked: hardcoded 98% oder Random 70–99%; echte Matching-Logik braucht F-400 PPP-Auth).
  • F14 / P2503-152 — Projekt-Alert (E-Mail-Subscription via simplenews oder Custom).

Verworfene Sub-Tasks

  • P2503-137 wurde initial als Mega-Sub-Task „Frontend: Detail- + List-View (Layer A + B)" angelegt — anti-Pattern, weil 1 Sub-Task = 1 AFK-Agent-Run. Wurde am 2026-05-14 zu Frontend F1 umgeschrieben, F2–F14 als 13 weitere Sub-Tasks angelegt. P2503-138/-139 sind Race-Condition-Duplikate (closed als obsolete,duplikat).

Lessons aus der Refinement-Session

  • Figma-Pixel-Realität dominiert PRD-Annahmen: User-Korrektur „Featured-Slot existiert nicht" am Anfang stoppte die naive Vacancy-Parität (card + featured + full). Sichtbares Card-Composite ist Variant der prop-driven card-SDC, nicht bubble-profile.
  • Die zwei „Das könnte Sie auch interessieren"-Karten sind kein Related-Publications-View-Block, sondern hardcoded SOLCOM-Marketing-Cross-Sell → eigener Custom-Block-Plugin (F6).
  • Frame-Realität enthält Features, die nicht im Backend-PRD stehen: Merkliste, Matching-Rate, Skills, Remote-Anteil-Slider, Suchradius, Projekt-Alert. Layer-A/Layer-B-Cut trennt MVP-Pflicht von Post-MVP-Vollparität.

Worklog (Implement-Loop)

Sequenzielle Bauphase auf Branch feature/P2503-108 via Skill /implement. Heavy-Brief-Pattern: Opus-Orchestrator setzt Trade-offs vorab, Sonnet-Sub-Agents implementieren.

  • P2503-128 — Module-Foundation (commit 36de8b1, status: Zu Review)
    Modul solcom_sf_publication mit 24 Feldern (gemäß Context-0008), Taxonomy sf_industry mit 25 Term-Seeds via hook_install, env-var-Snippet in example.settings.local.php, Editor-Boundary via leere Permission-Map. Vorab-Hook-Fix (c735eb1) für False-Positive auf example.settings.local.php.
  • P2503-129 — Auth-Foundation (commit e5eb771, status: Zu Review)
    drupal/key 1.22.0 aktiviert, 3 Key-Entities (env-provider) für FCSB-Credentials, SalesforceClient-Service mit State-cached Token + 401-Retry-Once-Logik, Kernel-Test deckt 4 Szenarien (Refresh-Success, Cache-Hit, 401-Retry, Persistent-401-Throw). Live-verifiziert gegen FCSB-Sandbox: getAccessToken() liefert Bearer-Token (env-vars aus DDEV web_environment).

Anpassungen vs. ursprünglichem Plan

  • P2503-127 + P2503-146 mit Label blocked-external markiert (Pre-Build-Gates für Live-Describe / Quota-Sizing, im Implement-Loop nicht autonom abarbeitbar — werden bei Bedarf manuell nachgezogen, sobald Auth-Foundation gegen FCSB-Sandbox grün ist; ab P2503-129 erfüllt).
  • Field-Count: Context-0008 dokumentiert 21 Felder, der Sub-Agent hat aus dem Mapping 24 Felder abgeleitet — die Differenz liegt an Spalten, die nur via SOQL-Query benötigt werden (z.B. RecordType, Name), aber kein eigenes Drupal-Storage brauchen.

Offene Fragen (Pre-Implementation-Gates)

  1. Steady-State Publication-Count: SELECT COUNT(Id) FROM TR1__Job__c WHERE RecordTypeId='012Wx000000C7ngIAC' gegen FCSB-Sandbox. Bestimmt Paging-Strategie und API-Quota-Sizing. Erste Implementations-Sub-Task.
  2. Live SF-Describe-Verifikation: /services/data/v59.0/sobjects/TR1__Job__c/describe gegen FCSB-Sandbox laufen, alle 22 Field-Namen aus Context-0008 verifizieren. Excel-Live-Sample bestätigt _date_time__c (mit _time) — Briefing-Drift-Verdacht widerlegt, aber Describe trotzdem Pflicht-Gate.
  3. Branch-Strategie: Lokaler Branch feature/P2503-108 trägt verworfenen Doc-Commit 791d974. Output dieser Refinement-Session läuft auf develop. Branch-Cleanup separat (Reset / Löschen ohne destruktive Operation ohne User-Autorisierung).
  4. Sub-Tasks-Refresh: RESOLVED 2026-05-14 — die ursprünglich angelegten P2503-127..134 (Backend) bleiben gültig. Frontend-Slice wurde als 14 granulare Sub-Tasks F1–F14 ergänzt (siehe Sektion Frontend-Decomposition).
  5. Phase-2-Abgrenzung: Registrierungs-Flow (manuell vs. automatisch), Merkliste (Drupal-Flag-Modul vs. SF-Sync) — beides Phase 2, wandert in F-290.B oder eigene Story.