Preuve de concept d'un flux evenementiel de bout en bout : une plateforme
externe (simulée ici par un script Python) publie un événement metier
(Payment_Failed__e ou Payment_Succeeded__e) sur le bus d'evenements
Salesforce. Chaque type d'evenement a son propre Flow abonne : l'echec de
paiement cree un Case de suivi et notifie l'equipe sur Slack, le succes
se contente d'une notification Slack (pas de suivi support necessaire).
┌──────────────────┐
│ Python Script │
│ External Event │ external-event-simulator/simulator.py
│ Simulator │
└─────────┬──────────┘
│ OAuth 2.0 Client Credentials
│ POST /services/data/vXX/sobjects/<Payment_Failed__e|Payment_Succeeded__e>/
▼
┌──────────────────┐
│ Salesforce API │
└─────────┬──────────┘
▼
┌───────────────────────────────────┐
│ Event Bus │
│ Payment_Failed__e / Succeeded__e │
└──────────┬──────────────┬──────────┘
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Payment_Failed - │ │ Payment_Succeeded - │
│ Handle_Event Flow │ │ Handle_Event Flow │
└─────────┬──────────┘ └──────────┬────────────┘
├─► Create Case (support) │
└─► Slack alert └─► Slack notification
Chaque objet Platform Event est un canal distinct, avec son propre schema de champs : un Flow ne peut s'abonner qu'a un seul objet a la fois, d'ou les deux Flows separes plutot qu'un unique Flow avec une branche de decision (voir aussi la section "Notes techniques").
force-app/main/default/
├── objects/Payment_Failed__e/ # Platform Event + 4 champs custom
├── objects/Payment_Succeeded__e/ # Platform Event + 3 champs custom
├── flows/Payment_Failed_Handle_Event # Flow declenche par l'echec : Case + Slack
├── flows/Payment_Succeeded_Handle_Event # Flow declenche par le succes : Slack uniquement
├── permissionsets/
│ ├── Payment_Failed_Event_Integration # Acces au Platform Event Failed + classes Slack
│ └── Payment_Succeeded_Event_Integration # Acces au Platform Event Succeeded + classes Slack
├── customMetadata/
│ ├── SSR_Slack_Recipe.recipe_payment_failed # Canal Slack pour l'alerte d'echec
│ └── SSR_Slack_Recipe.recipe_payment_succeeded # Canal Slack pour la notif de succes
├── classes/Slack*.cls # Stack Slack existante (recuperee depuis
│ # MY_DEV_ORG / salesforce-slack-recipes)
├── objects/SSR_Slack_Settings__c/ # Bot token Slack (Custom Setting)
├── objects/SSR_Slack_Recipe__mdt/ # Mapping recette -> canal Slack
├── permissionsets/SSR_Slack_User # Acces Slack existant
├── remoteSiteSettings/SSR_Slack # Remote Site pour l'API Slack
└── connectedApps/
└── Payment_Event_Integration # OAuth Client Credentials Flow
# (deploie tel quel sur la sandbox ;
# bloque sur MY_DEV_ORG, voir section 3)
external-event-simulator/
├── simulator.py # Script Python "plateforme externe" (--event-type payment_failed|payment_succeeded)
├── requirements.txt
└── .env.example
docs/reference-metadata/connectedApps/
└── Payment_Event_Integration.connectedApp-meta.xml
# Version de reference sans consumerKey/Secret, pour MY_DEV_ORG
# (documentation de la config cible a recreer manuellement)
Champs du Platform Event Payment_Failed__e :
| Champ API | Type | Description |
|---|---|---|
External_Order_Id__c |
Text | Identifiant de commande sur la plateforme externe |
Status__c |
Text | Statut metier (ex. PAYMENT_FAILED) |
Customer_Email__c |
Text | Email du client |
Amount__c |
Number | Montant de la transaction |
Champs du Platform Event Payment_Succeeded__e (pas de Status__c, le nom
de l'objet porte deja l'information) :
| Champ API | Type | Description |
|---|---|---|
External_Order_Id__c |
Text | Identifiant de commande sur la plateforme externe |
Customer_Email__c |
Text | Email du client |
Amount__c |
Number | Montant de la transaction |
- Salesforce CLI (
sf) - Un org connecte en local :
sf org list - Python 3.9+
Ce projet a ete deploye et valide de bout en bout sur deux orgs, avec deux comportements differents concernant le Connected App :
| Org | Alias | Connected App via Metadata API |
|---|---|---|
| Developer Edition (Trailhead) | MY_DEV_ORG |
Bloqué (You can't create a connected app...) — creation manuelle requise, voir section dediee plus bas |
| Sandbox d'entreprise | MY_SANDBOX |
OK, deploiement 100% automatise, Client Credentials Flow actif immediatement |
La stack Slack (SlackInvocable, SlackService, SSR_Slack_Recipe__mdt,
SSR_Slack_Settings__c, ...) est deja presente sur les deux orgs (issue de
salesforce-slack-recipes).
Sur la sandbox d'entreprise elle est activement utilisee par 8 autres
recettes Slack (recipe_01 a recipe_09) : on ne redeploie donc jamais ces
composants existants, uniquement les nouveaux elements propres a ce POC
(les deux Platform Events, les deux Flows, les Permission Sets, les
recettes CMDT recipe_payment_failed / recipe_payment_succeeded, et le
Connected App).
Ce README et les fichiers de metadata versionnes (connectedApps/,
.env.example) utilisent des placeholders generiques a la place des
vraies valeurs de mes orgs (alias, domaines My Domain, usernames, nom
d'entreprise) :
| Placeholder | A remplacer par |
|---|---|
MY_DEV_ORG |
l'alias de votre org Developer Edition / Trailhead (sf org list) |
MY_SANDBOX |
l'alias de votre sandbox |
your-dev-org-dev-ed.trailblaze.my.salesforce.com |
le domaine My Domain de votre org Dev (visible via sf org display --target-org <alias>) |
your-sandbox.my.salesforce.com |
le domaine My Domain de votre sandbox |
admin@your-dev-org.com / REMPLACER_PAR_VOTRE_USERNAME_* |
l'username de l'utilisateur d'integration |
Aucune valeur reelle (org, alias, domaine, email, secret) n'est commitee
dans ce repo. Le fichier external-event-simulator/.env (avec vos vraies
valeurs) reste local et est exclu par .gitignore.
sf project deploy start --target-org MY_DEV_ORG --source-dir force-app
sf org assign permset --target-org MY_DEV_ORG --name Payment_Failed_Event_Integration
sf org assign permset --target-org MY_DEV_ORG --name Payment_Succeeded_Event_IntegrationSur cet org le dossier force-app contient toute la stack Slack (car elle a
ete recupérée et versionnée depuis MY_DEV_ORG au debut du projet) : le déploiement
complet est donc sans risque.
Sur cet org, la stack Slack existe deja et est partagee avec d'autres automatisations : on deploie uniquement les nouveaux composants, en excluant les classes/objects/permsets Slack deja presents.
Avant de deployer, personnalisez
force-app/main/default/connectedApps/Payment_Event_Integration.connectedApp-meta.xml
(livre avec des valeurs placeholder, voir les commentaires dans le fichier) :
contactEmail, callbackUrl (domaine de votre org), oauthClientCredentialUser
(votre utilisateur d'integration), et generez un consumerKey/consumerSecret :
python3 -c "import secrets,string; a=string.ascii_letters+string.digits; \
print('consumerKey:', ''.join(secrets.choice(a) for _ in range(64))); \
print('consumerSecret:', ''.join(secrets.choice(a) for _ in range(48)))"SANDBOX_ALIAS='MY_SANDBOX'
sf project deploy start --target-org "$SANDBOX_ALIAS" \
--source-dir force-app/main/default/objects/Payment_Failed__e \
--source-dir force-app/main/default/objects/Payment_Succeeded__e \
--source-dir force-app/main/default/flows/Payment_Failed_Handle_Event.flow-meta.xml \
--source-dir force-app/main/default/flows/Payment_Succeeded_Handle_Event.flow-meta.xml \
--source-dir force-app/main/default/permissionsets/Payment_Failed_Event_Integration.permissionset-meta.xml \
--source-dir force-app/main/default/permissionsets/Payment_Succeeded_Event_Integration.permissionset-meta.xml \
--source-dir force-app/main/default/customMetadata/SSR_Slack_Recipe.recipe_payment_failed.md-meta.xml \
--source-dir force-app/main/default/customMetadata/SSR_Slack_Recipe.recipe_payment_succeeded.md-meta.xml \
--source-dir force-app/main/default/connectedApps/Payment_Event_Integration.connectedApp-meta.xml
sf org assign permset --target-org "$SANDBOX_ALIAS" --name Payment_Failed_Event_Integration
sf org assign permset --target-org "$SANDBOX_ALIAS" --name Payment_Succeeded_Event_IntegrationSur cette sandbox, le Connected App se deploie et s'active (Client Credentials Flow) directement via ce deploiement, sans etape manuelle : la section 3 ci-dessous ne concerne que l'org MY_DEV_ORG (ou utilisez le mode session de la section 2).
Les Custom Metadata recipe_payment_failed et recipe_payment_succeeded
pointent par defaut vers le meme canal que recipe_01 sur l'org cible
(valeur d'exemple C0XXXXXXXXX, identique sur les deux orgs de ce POC).
Verifiez/adaptez le Channel ID dans Setup > Custom Metadata Types >
SSR Slack Recipe selon vos besoins.
Sur MY_DEV_ORG, la creation de Connected App est definitivement impossible
(restriction Trailhead Playground, cf. section 3). En attendant — ou a la
place — simulator.py supporte un mode session qui reutilise un access
token deja obtenu (ex. celui du Salesforce CLI) au lieu de faire
l'authentification OAuth Client Credentials.
Ce n'est pas une simulation de l'authentification "plateforme externe" (le CLI a sa propre session), mais ca permet de tester le vrai code Python et toute la chaine Event -> Flow -> (Case) -> Slack, sans aucune configuration supplementaire :
export SF_INSTANCE_URL="https://my-dev-org-dev-ed.trailblaze.my.salesforce.com"
export SF_ACCESS_TOKEN="$(SF_TEMP_SHOW_SECRETS=true sf org display --target-org MY_DEV_ORG --json | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["accessToken"])')"
cd external-event-simulator
python simulator.py --event-type payment_failed --order-id ORD-2026-042 --status PAYMENT_FAILED --email jane@example.com --amount 500
python simulator.py --event-type payment_succeeded --order-id ORD-2026-042 --email jane@example.com --amount 500
unset SF_ACCESS_TOKEN SF_INSTANCE_URLsimulator.py détecte automatiquement SF_ACCESS_TOKEN / SF_INSTANCE_URL
et bascule en mode session (affiche Mode session (bypass OAuth)...) sans
toucher a SF_CONSUMER_KEY / SF_CONSUMER_SECRET. Le token du CLI expire
au bout de quelques heures : relancez la commande export SF_ACCESS_TOKEN=...
si besoin.
Alternative équivalente sans le script Python, directement en ligne de commande (ce qui a servi a la toute premiere validation de ce POC) :
sf api request rest 'services/data/v67.0/sobjects/Payment_Failed__e/' \
--target-org MY_DEV_ORG -X POST \
-b '{"External_Order_Id__c":"ORD-2026-042","Status__c":"PAYMENT_FAILED","Customer_Email__c":"jane@example.com","Amount__c":500}'
sf api request rest 'services/data/v67.0/sobjects/Payment_Succeeded__e/' \
--target-org MY_DEV_ORG -X POST \
-b '{"External_Order_Id__c":"ORD-2026-043","Customer_Email__c":"jane@example.com","Amount__c":500}'Optionnel si vous utilisez le mode session decrit en section 2 : sur la sandbox d'entreprise, le Connected App est deja deploye automatiquement a l'etape 1, cette section ne concerne donc qu'MY_DEV_ORG.
Sur cette org (Developer Edition / Trailhead Playground), la création de Connected App via l'API Metadata est bloquee par Salesforce :
You can't create a connected app. To enable connected app creation,
contact Salesforce Customer Support.
C'est une restriction courante sur ce type d'org (anti-abus) : la creation
doit se faire depuis l'UI Setup. La configuration cible est documentee dans
docs/reference-metadata/connectedApps/Payment_Event_Integration.connectedApp-meta.xml.
Etapes (~5 minutes) :
- Setup > App Manager > New Connected App
- Basic Information :
- Connected App Name :
Payment Event Integration - Contact Email : le votre
- Connected App Name :
- API (Enable OAuth Settings) :
- Cocher Enable OAuth Settings
- Callback URL :
https://login.salesforce.com/services/oauth2/callback(non utilise par le flow Client Credentials, mais requis par le formulaire) - Selected OAuth Scopes : ajouter Manage user data via APIs (api)
- Save (la propagation peut prendre quelques minutes)
- Retourner sur l'app > Manage > Edit Policies :
- Section OAuth Policies > cocher Enable Client Credentials Flow
- Run As : choisissez l'utilisateur d'integration (ex. l'admin
admin@your-dev-org.com). Cet utilisateur doit avoir les Permission SetsPayment_Failed_Event_IntegrationetPayment_Succeeded_Event_Integrationassignes (deja fait a l'etape 1 pour l'admin). - Save
- Retourner sur la page de l'app > Manage Consumer Details (un code de verification est envoye par email) > copier Consumer Key et Consumer Secret.
cd external-event-simulator
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Renseigner SF_DOMAIN, SF_CONSUMER_KEY, SF_CONSUMER_SECRET dans .envSF_DOMAIN est le domaine My Domain de l'org, sans https://, par exemple
my-dev-org-dev-ed.trailblaze.my.salesforce.com (visible via
sf org display --target-org MY_DEV_ORG).
Publier un événement d'echec de paiement :
python simulator.py \
--event-type payment_failed \
--order-id ORD-2026-001 \
--status PAYMENT_FAILED \
--email customer@example.com \
--amount 750Publier un événement de succes de paiement (--event-type par defaut est
payment_failed, --status est ignore pour payment_succeeded) :
python simulator.py \
--event-type payment_succeeded \
--order-id ORD-2026-002 \
--email customer@example.com \
--amount 750Ou générer plusieurs evenements aleatoires (echecs, par defaut) :
python simulator.py --random --count 5 --delay 2
python simulator.py --event-type payment_succeeded --random --count 5 --delay 2sf data query --target-org <alias-de-votre-org> \
--query "SELECT Id, Subject, Status, Priority, SuppliedEmail, CreatedDate FROM Case ORDER BY CreatedDate DESC LIMIT 5"Pour payment_failed : un nouveau Case "Echec de paiement - Commande
ORD-2026-001" doit apparaitre, et un message doit etre poste sur le canal
Slack configure dans recipe_payment_failed.
Pour payment_succeeded : pas de Case (comportement voulu), uniquement un
message Slack poste sur le canal configure dans recipe_payment_succeeded.
Dans les deux cas, l'execution du Flow est verifiable via
SELECT ApexClass.Name, Status, NumberOfErrors FROM AsyncApexJob WHERE ApexClass.Name = 'SlackCalloutQueueable' ORDER BY CreatedDate DESC LIMIT 5.
Le job SlackCalloutQueueable se termine sans erreur meme si l'appel a
l'API Slack echoue (l'exception est capturée et transformee en
PostMessageResult(success=false, ...) sans etre relancee). Autrement dit,
"job Completed avec 0 erreur" ne garantit pas que le message Slack a bien
ete poste, seulement que le Flow + Apex se sont executes sans planter.
Si votre compte/app Slack a ete desactive, une fois le nouveau bot cree :
- Recupérez le nouveau Bot User OAuth Token (
xoxb-...) avec le scopechat:write. - Setup > Custom Settings > SSR Slack Settings > Manage > mettez a jour
Bot_Token__c(org default). Ce token est partage par toutes les recettes SSR sur l'org. - Invitez le bot dans le canal cible, puis mettez a jour si besoin le
Channel_Id__cdes recettesrecipe_payment_failedetrecipe_payment_succeeded(Setup > Custom Metadata Types > SSR Slack Recipe). Par defaut elles pointent vers le meme canal querecipe_01sur votre org. - Retestez avec
python simulator.py(les deux--event-type) et verifiez la reception des messages dans Slack.
- Les Platform Events sont de type High Volume,
publishBehavior: PublishAfterCommit. - Un Flow declenche par Platform Event est lie a un seul objet au
moment de sa creation (
triggerType: PlatformEvent+ unobjectcible unique) : il n'est pas possible d'avoir un seul Flow abonne a la fois aPayment_Failed__eetPayment_Succeeded__eavec une branche de decision interne. C'est pourquoi ce POC a deux Flows distincts, chacun abonne a son propre canal — c'est le typage fort des Platform Events qui impose cette separation, et qui garantit en echange qu'aucun message mal forme ou ambigu ne peut transiter sur le bus. - Les Flows
Payment_Failed_Handle_EventetPayment_Succeeded_Handle_Eventsont des Autolaunched Flows avectriggerType: PlatformEvent. Ils s'executent dans le contexte de l'utilisateur qui a publie l'evenement (d'ou l'importance des Permission Sets assignes a l'utilisateur d'integration). - La notification Slack reutilise la stack
SlackInvocable/SlackServicedéjà presente sur l'org (issue du reposalesforce-slack-recipes), recuperee dans ce projet pour etre versionnee avec le reste du POC. - Le test end-to-end initial de ce POC a ete valide avec
sf api request rest(session admin CLI) avant meme la creation du Connected App, pour confirmer que la chaine Event -> Flow -> Case -> Slack fonctionnait independamment de l'authentification cote script Python.