Skip to content

Repository files navigation

POC Salesforce Platform Events — Payment Failed / Payment Succeeded

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).

Architecture

┌──────────────────┐
│  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").

Contenu du repo

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

Prerequis

  • Salesforce CLI (sf)
  • Un org connecte en local : sf org list
  • Python 3.9+

Orgs cibles de ce POC

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).

Anonymisation

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.

1. Deployer le projet

Sur MY_DEV_ORG (Developer Edition) — Slack a redeployer, Connected App exclu

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_Integration

Sur 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 une Sandbox d'entreprise — deploiement cible (additif uniquement)

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)))"

⚠️ Ne commitez pas ce fichier une fois rempli avec vos vraies valeurs (voir "Depersonnalisation" plus bas). Si le Connected App est deja deploye sur votre org, inutile de repasser cette commande — elle ne sert qu'au tout premier deploiement.

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_Integration

Sur 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.

2. Tester sans Connected App (mode session, recommande pour MY_DEV_ORG)

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_URL

simulator.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}'

3. Creer le Connected App manuellement (uniquement pour MY_DEV_ORG, optionnel)

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) :

  1. Setup > App Manager > New Connected App
  2. Basic Information :
    • Connected App Name : Payment Event Integration
    • Contact Email : le votre
  3. 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)
  4. Save (la propagation peut prendre quelques minutes)
  5. 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 Sets Payment_Failed_Event_Integration et Payment_Succeeded_Event_Integration assignes (deja fait a l'etape 1 pour l'admin).
    • Save
  6. Retourner sur la page de l'app > Manage Consumer Details (un code de verification est envoye par email) > copier Consumer Key et Consumer Secret.

4. Configurer et lancer le simulateur Python (mode OAuth)

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 .env

SF_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 750

Publier 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 750

Ou 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 2

5. Verifier le résultat

sf 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.

Bot Slack a reconfigurer

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 :

  1. Recupérez le nouveau Bot User OAuth Token (xoxb-...) avec le scope chat:write.
  2. 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.
  3. Invitez le bot dans le canal cible, puis mettez a jour si besoin le Channel_Id__c des recettes recipe_payment_failed et recipe_payment_succeeded (Setup > Custom Metadata Types > SSR Slack Recipe). Par defaut elles pointent vers le meme canal que recipe_01 sur votre org.
  4. Retestez avec python simulator.py (les deux --event-type) et verifiez la reception des messages dans Slack.

Notes techniques

  • 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 + un object cible unique) : il n'est pas possible d'avoir un seul Flow abonne a la fois a Payment_Failed__e et Payment_Succeeded__e avec 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_Event et Payment_Succeeded_Handle_Event sont des Autolaunched Flows avec triggerType: 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 / SlackService déjà presente sur l'org (issue du repo salesforce-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.

About

Platform Event POC with Python Script mocks

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages