Architecture Decision Record · 0026

solcom_sf_publication als schlankes Custom-Modul statt Drupal Salesforce Suite

TL;DR — Für den Pull von Salesforce-Publikationen (BH4SF, Story F-290 / P2503-108) entsteht ein neues Custom-Modul web/modules/custom/solcom_sf_publication mit ~300 LoC: Guzzle-basierter Service-OAuth-Client (refresh_token-Flow), SOQL-Delta-Puller, Field-Mapper inkl. HTML-Sanitize, QueueWorker für Pull, Reconciliation und Outbound-URL-Push, plus Drush-Commands. Die Drupal Salesforce Suite (drupal/salesforce + ~15 Sub-Module) wird nicht installiert.

Kontext

Story F-290 verlangt einen unidirektionalen Pull von Projekt-Ausschreibungen (TR1__Job__c mit RecordType Publication) aus der FCSB-Sandbox in die SOLCOM-Website. Zusätzlich schreibt Drupal ein einziges Feld zurück: hohr__Job_Application_URL__c, gesetzt bei Publish und gelöscht bei Unpublish (siehe Context-0008). User-Authentifizierung über SF, User-Linking, Profile-Pull oder Merkliste sind nicht in dieser Story — Drupal-Login bleibt nativ (F-400, separate Story).

Damit ist der Scope für einen SF-Connector sehr eng:

Der etablierte Drupal-Weg für SF-Integrationen ist das Contrib-Modul drupal/salesforce („Salesforce Suite") mit ~15 Sub-Modulen: salesforce_mapping, salesforce_push, salesforce_pull, salesforce_logger, salesforce_jwt, salesforce_oauth … Es bietet eine umfangreiche Mapping-UI, Queues für Push/Pull, Webhook-Endpoints, JWT-Auth und sieht für jeden Drupal-Field-Typ einen Plugin-Slot vor.

Das Scope-zu-Suite-Verhältnis ist asymmetrisch: F-290 nutzt davon ~5% der Surface (genau einen Pull, einen Push, ein Mapping). Der Rest ist Footprint, der über die nächsten Jahre gepflegt, geupdatet und verstanden werden muss. Karpathy-Guideline: Minimum code that solves the problem. Nothing speculative.

Entscheidung

Wir bauen solcom_sf_publication als schlankes Custom-Modul. Die Drupal Salesforce Suite wird nicht installiert. Konkrete Modul-Struktur (folgt drupal-module-development-Konventionen):

web/modules/custom/solcom_sf_publication/
├─ solcom_sf_publication.info.yml
├─ solcom_sf_publication.module        (hook_node_update → Queue-Enqueue)
├─ solcom_sf_publication.install       (field_sf_id unique-index, content-type-Trigger)
├─ solcom_sf_publication.services.yml
├─ solcom_sf_publication.routing.yml   (Admin-Status-Page)
├─ solcom_sf_publication.permissions.yml
├─ config/install/                     (Default-Settings, Cron-Period)
├─ config/schema/
└─ src/
   ├─ Service/
   │  ├─ SfClient.php                  (Guzzle, OAuth refresh, 429-Retry)
   │  ├─ PublicationPuller.php         (SOQL, Pagination via nextRecordsUrl)
   │  ├─ PublicationMapper.php         (SF-Felder → Drupal-Felder, Industry-Lookup)
   │  ├─ RichTextSanitizer.php         (Whitelist, force.com-Filter)
   │  └─ ReconciliationService.php     (Full-Id-Diff)
   ├─ Plugin/QueueWorker/
   │  ├─ PullDeltaWorker.php           (#[QueueWorker(cron: ['time'=>120])])
   │  ├─ ProcessPublicationWorker.php  (Item-für-Item Upsert)
   │  ├─ ReconcileWorker.php
   │  └─ PushUrlWorker.php
   ├─ EventSubscriber/                 (optional: Logging, Health)
   ├─ Commands/
   │  └─ SolcomSfCommands.php          (drush solcom-sf:bootstrap|reconcile|push-urls)
   └─ Controller/
      └─ StatusController.php          (/admin/reports/solcom-sf)

Operative Anker:

!
SOLCOM hat genau einen SF-Konsumenten in Sicht. Diese Entscheidung ist explizit nicht für „SF-Integrationen im Allgemeinen" — wenn ein zweiter Konsument auftaucht (z.B. Marketing-Cloud-Lead-Sync), wird die Entscheidung revisited und ein gemeinsamer Auth-Service extrahiert. „Rule of Three" — Abstraktion frühestens beim dritten Vorkommen.

Rejected alternatives

Drupal Salesforce Suite (drupal/salesforce) komplett rejected

Bewährt, breite Surface (Mapping-UI, Push-/Pull-Queues, JWT, OAuth, Webhook-Endpunkte). Verworfen, weil das Scope-zu-Footprint-Verhältnis nicht passt: F-290 nutzt ~5% der Module. Maintenance- und Update-Last des großen Sub-Modul-Sets übersteigt den Mehrwert; Mapping-UI ersetzt nicht den Source-of-Truth-Vorteil von versioniertem PHP-Mapping; Plugin-Hooks würden für jede Anpassung (RichText-Sanitize, Lifecycle-Window-Logik, Window+Status-driven Publish-Status) durchgriffen werden müssen.

Reaktivierung der Entscheidung sinnvoll, wenn (a) ≥3 SF-Objekte synchronisiert werden, (b) Site-Builder das Mapping in der UI pflegen sollen, oder (c) bi-direktionale Sync für ≥2 Felder gewünscht ist.

Hybrid — Auth aus der Suite, Mapping/Push/Pull custom rejected

Idee: nur salesforce-Core inkl. salesforce_oauth für Token-Handling installieren, Mapping/Push/Pull custom bauen. Verworfen, weil die Suite ein integriertes Modul-Set ist — das Auth-Modul allein zieht die RestClient-Plumbing, Config-Entities, ein eigenes Logger-Modul und ein Set von Service-Definitionen mit. Die geretteten ~30 LoC OAuth-Refresh aus dem Custom-Service stehen einer halben Welt aus Suite-Configs/Services gegenüber.

Bestehende Drupal-Suite-Fork mit nur den nötigen Sub-Modulen vendoren rejected

Schneller Start, aber Patch-Stream gegen Upstream wird zur permanenten Last. Custom-Modul ist schlanker und nicht von Upstream-Releases abhängig.

Boomi-Webhook-Endpoint in Drupal statt Pull rejected

Excel-Header („Boomi Canonical Data Model") und Sample-Payload legen einen Pfad SF → Boomi → Drupal-Webhook nahe. Verworfen für Phase 1, weil (a) Kunde Drupal direkte FCSB-OAuth-Creds gegeben hat (Indikator: Pull-Wunsch), (b) Boomi-Verfügbarkeit/-Verantwortung nicht in SOLCOMs Scope. Detail-Begründung siehe ADR-0027.

Consequences