À proposExpertiseProjetsParcoursBlogContact
Discuter
À proposExpertiseProjetsParcoursBlogContactDiscuter

Yao David Logan

Software Engineer fullstack spécialisé en SaaS, automatisation métier et plateformes web/mobile scalables.

NavigationExpertiseProjetsParcoursBlogContact
LiensGitHubLinkedInEmail
© 2026 Yao David Logan. Tous droits réservés.
Paiement multi-passerelles et chiffrement E2E — le pattern qui ne casse pas
Retour au blog

Tech

Xin@

Paiement multi-passerelles et chiffrement E2E — le pattern qui ne casse pas

YDLYao David Logan18 min de lecture18 septembre 2026

CatégorieTech

Lecture18 min de lecture

Publié18 septembre 2026

Vues0

Partages0

Commentaires0

Sommaire
  1. 011. Le contexte
  2. 022. Pourquoi le naïf casse
  3. 033. Le pattern Gateway Adapter
  4. 044. Le chiffrement bout en bout — pour de vrai
  5. 055. Idempotence native — la fondation
  6. 066. Les webhooks — où tout dérape
  7. 077. La réconciliation — votre filet de sécurité
  8. 088. Audit trail — ce que l'auditeur va demander
  9. 099. Pièges à éviter
  10. 1010. Conclusion

Adapter pattern, idempotence native, chiffrement bout en bout côté client, webhooks séparés et table d'audit append-only : le squelette pour intégrer six passerelles de paiement sans dette.

Intégrer plusieurs passerelles de paiement dans une plateforme fintech, c'est facile. Le faire sans incident en production, c'est une autre histoire. Voici le pattern que j'utilise.

1. Le contexte

Sur Nexus, la plateforme accepte plusieurs moyens de paiement : carte bancaire, mobile money (MTN, Orange, Moov), virement bancaire, parfois crypto. Chaque passerelle vit dans son écosystème : son SDK, son format de webhook, sa logique d'idempotence, son régime de pannes. Sans organisation claire, l'intégration finit en spaghetti de if imbriqués qu'aucun nouveau développeur ne comprend.

L'objectif de cet article : présenter le pattern qui m'a permis d'intégrer six passerelles distinctes sans dupliquer une seule ligne de logique métier, avec chiffrement bout en bout et idempotence native.

2. Pourquoi le naïf casse

Première tentation : un gros service PaymentService avec un switch sur le moyen demandé. À 2 passerelles ça marche, à 6 c'est ingérable. Symptômes :

  • Bug sur Orange MoMo qui se propage à MTN parce que les méthodes partagent un cache
  • Webhook Stripe qui plante parce qu'on a oublié d'updater le switch quand on a ajouté Mobile Money
  • Tests d'intégration qui montent une mégafixture pour tester un seul moyen
  • Migration de version d'API de l'une force un redéploiement de toutes

La règle : un moyen de paiement = un module isolé, branché par contrat.

3. Le pattern Gateway Adapter

Le service paiement ne connaît qu'une interface : PaymentGateway. Chaque passerelle fournit son implémentation. Le routing est centralisé, le reste est polymorphe.

TypeScript
1// src/services/payment/types.ts
2export type PaymentMethod =
3 | "card"
4 | "mobile_money_mtn"
5 | "mobile_money_orange"
6 | "mobile_money_moov"
7 | "bank_transfer"
8 | "crypto_usdc";
9 
10export interface PaymentInitiateInput {
11 amount: number;
12 currency: "XOF" | "USD" | "EUR";
13 customerId: string;
14 method: PaymentMethod;
15 reference: string; // référence métier (no PII)
16 metadata?: Record<string, string>;
17}
18 
19export interface PaymentSession {
20 sessionId: string;
21 redirectUrl?: string;
22 expiresAt: Date;
23}
24 
25export interface PaymentGateway {
26 readonly name: string;
27 readonly supports: ReadonlyArray<PaymentMethod>;
28 initiate(input: PaymentInitiateInput): Promise<PaymentSession>;
29 confirm(sessionId: string, evidence: WebhookPayload): Promise<PaymentResult>;
30 refund(transactionId: string, amount: number): Promise<RefundResult>;
31}

Chaque adapter expose ses méthodes supportées via supports. Le router central choisit le bon en fonction de la méthode demandée :

TypeScript
1// src/services/payment/PaymentRouter.ts
2import { PaymentGateway, PaymentMethod } from "./types.js";
3 
4export class PaymentRouter {
5 private readonly gateways = new Map<PaymentMethod, PaymentGateway>();
6 
7 register(gateway: PaymentGateway) {
8 for (const method of gateway.supports) {
9 if (this.gateways.has(method)) {
10 throw new Error(`Method ${method} already registered by ${this.gateways.get(method)!.name}`);
11 }
12 this.gateways.set(method, gateway);
13 }
14 }
15 
16 resolve(method: PaymentMethod): PaymentGateway {
17 const gateway = this.gateways.get(method);
18 if (!gateway) throw new Error(`No gateway registered for method ${method}`);
19 return gateway;
20 }
21}

Pour ajouter une 7ème passerelle : on écrit l'adapter, on l'enregistre au démarrage, le reste du code n'a pas à savoir qu'elle existe.

4. Le chiffrement bout en bout — pour de vrai

"Chiffrement E2E" est un mot abusé. Sur une plateforme de paiement, ça veut dire concrètement : les données sensibles (PAN, CVV, IBAN, numéro mobile) ne touchent jamais notre serveur en clair. Elles sont chiffrées côté client avec une clé publique de la passerelle, transmises chiffrées à travers notre backend, et déchiffrées seulement par la passerelle finale.

Côté frontend :

TypeScript
1// frontend/src/lib/payment/encryptPan.ts
2export async function encryptPan(pan: string, gatewayPublicKeyPem: string): Promise<string> {
3 const key = await importPublicKey(gatewayPublicKeyPem);
4 const buffer = new TextEncoder().encode(pan);
5 const encrypted = await crypto.subtle.encrypt(
6 { name: "RSA-OAEP" },
7 key,
8 buffer,
9 );
10 return arrayBufferToBase64(encrypted);
11}

Côté backend, le PAN chiffré transite tel quel jusqu'à l'API de la passerelle. Aucun log, aucune persistance, aucun hash. Le seul élément qui reste de notre côté est un token opaque retourné par la passerelle, valide pour les remboursements futurs.

Cette approche élimine 80 % des risques PCI-DSS. Le serveur ne stocke pas de données carte, ne peut pas en émettre une copie, et ne peut pas être compromise pour fuiter des PAN.

5. Idempotence native — la fondation

Toute opération de paiement doit être idempotente. Le client doit pouvoir rejouer la même requête 200 fois sans créer 200 paiements. La discipline :

TypeScript
1// src/controllers/payment/initiatePayment.ts
2export async function initiatePayment(req: Request, res: Response) {
3 const idempotencyKey = req.header("idempotency-key");
4 if (!idempotencyKey) {
5 return res.status(400).json({ error: "idempotency-key header required" });
6 }
7 
8 // 1. Existe-t-il déjà une session pour cette clé ?
9 const cached = await IdempotencyCache.findOne({ where: { key: idempotencyKey } });
10 if (cached) {
11 return res.status(cached.statusCode).json(cached.response);
12 }
13 
14 // 2. Sinon, on traite la requête
15 const session = await paymentRouter
16 .resolve(req.body.method)
17 .initiate(req.body);
18 
19 const response = { sessionId: session.sessionId, redirectUrl: session.redirectUrl };
20 
21 // 3. On cache la réponse pour 24 h
22 await IdempotencyCache.create({
23 key: idempotencyKey,
24 statusCode: 201,
25 response,
26 expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
27 });
28 
29 res.status(201).json(response);
30}

Le client génère un UUID v7 (ordonnable temporellement) par tentative. S'il rejoue à cause d'une déconnexion, il renvoie la même clé. Le serveur retourne la même réponse. Pas de double paiement.

6. Les webhooks — où tout dérape

Chaque passerelle envoie ses webhooks à son rythme et avec son format. Le piège classique : tenter de tout normaliser dans un seul endpoint. Mauvaise idée. Préférer un endpoint par passerelle avec son propre validateur de signature.

TypeScript
1// src/routes/webhooks.ts
2router.post("/webhooks/mtn", verifyMtnSignature, mtnWebhookHandler);
3router.post("/webhooks/orange", verifyOrangeSignature, orangeWebhookHandler);
4router.post("/webhooks/stripe", verifyStripeSignature, stripeWebhookHandler);

Chaque handler convertit son format propriétaire en PaymentEvent normalisé, persiste l'event, accuse réception immédiatement (200 OK dans les 200 ms), et dispatch un job en queue pour le traitement réel.

TypeScript
1// src/middlewares/verifyStripeSignature.ts
2import Stripe from "stripe";
3 
4const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
5const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
6 
7export function verifyStripeSignature(req: Request, res: Response, next: NextFunction) {
8 const signature = req.header("stripe-signature");
9 if (!signature) return res.status(400).json({ error: "missing signature" });
10 try {
11 const event = stripe.webhooks.constructEvent(req.rawBody, signature, webhookSecret);
12 req.body = event; // event vérifié
13 next();
14 } catch (error) {
15 return res.status(400).json({ error: "invalid signature" });
16 }
17}

Important : utiliser req.rawBody (avant parsing JSON) pour la vérification de signature — sinon ça casse à cause des espaces ajoutés/retirés par le parser.

7. La réconciliation — votre filet de sécurité

Aucun webhook n'est garanti. Une passerelle peut tomber, votre serveur peut être hors ligne, le réseau peut perdre des paquets. La réconciliation périodique est obligatoire : un job qui interroge chaque passerelle toutes les 15 minutes pour récupérer les transactions des 24 dernières heures et croiser avec votre base.

TypeScript
1// src/jobs/reconcilePayments.ts
2export async function reconcilePayments(gateway: PaymentGateway) {
3 const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
4 const remoteTxs = await gateway.listTransactions({ since });
5 
6 for (const remote of remoteTxs) {
7 const local = await Transaction.findOne({
8 where: { gateway: gateway.name, externalId: remote.id },
9 });
10 
11 if (!local) {
12 logger.warn("Missing local transaction", { remote });
13 await createTransactionFromRemote(gateway, remote);
14 } else if (local.status !== remote.status) {
15 logger.warn("Status mismatch", { local: local.status, remote: remote.status });
16 await local.update({ status: remote.status, reconciledAt: new Date() });
17 }
18 }
19}

La réconciliation a sauvé Nexus deux fois lors de coupures du fournisseur mobile money : les paiements confirmés mais non notifiés ont été récupérés sans intervention manuelle.

8. Audit trail — ce que l'auditeur va demander

Toute opération financière doit être tracée. Pas pour le marketing, pour la conformité. À chaque mutation d'un solde ou d'une transaction, on écrit dans une table payment_audit :

SQL
1CREATE TABLE payment_audit (
2 id UUID PRIMARY KEY,
3 transaction_id UUID NOT NULL,
4 actor_type VARCHAR(20) NOT NULL, -- 'user' | 'system' | 'admin'
5 actor_id UUID NULL,
6 action VARCHAR(40) NOT NULL, -- 'initiated' | 'confirmed' | 'refunded' | ...
7 before_state JSONB NULL,
8 after_state JSONB NOT NULL,
9 trace_id VARCHAR(64) NOT NULL,
10 at TIMESTAMP NOT NULL DEFAULT NOW()
11);
12CREATE INDEX ON payment_audit (transaction_id, at);

Cette table est append-only. Aucun UPDATE, aucun DELETE. Toute tentative est refusée par un trigger Postgres. C'est la base sur laquelle un auditeur reconstruit l'historique d'une transaction.

9. Pièges à éviter

PiègeSymptômeCorrection
Stocker le PAN même chiffréRisque PCI-DSS, audit lourdChiffrement côté client, jamais touché côté serveur
Endpoint webhook uniqueCouplage entre passerellesUn endpoint par passerelle, signature spécifique
Pas de idempotency-keyDoubles paiements en cas de retryHeader obligatoire, cache 24 h
Réconciliation manuelleTransactions perdues lors de coupuresJob périodique de réconciliation
Audit dans la même tablePollution + lockTable payment_audit dédiée, append-only
Logs avec PANFuite massive en cas de breachFiltrage des champs sensibles avant log

10. Conclusion

Le paiement multi-passerelles n'est pas un problème technique difficile. C'est un problème de discipline : isoler chaque passerelle, normaliser les events, idempotence partout, audit trail intangible, chiffrement bout en bout pour de vrai.

Si vous démarrez aujourd'hui, prenez ce squelette : PaymentGateway interface, PaymentRouter central, endpoints webhooks séparés, idempotency-key obligatoire, table d'audit append-only, job de réconciliation. Tout le reste est de la plomberie spécifique à chaque passerelle.

Commentaires

Réactions des lecteurs

Aucun commentaire pour l'instant

Aucun spam — l'email sert uniquement à vérifier votre identité.

·

Soyez le premier à réagir à cet article.

←Précédent60 fps sur Android entrée de gamme — guide perfs React Native pour les marchés émergentsTutoriel · 17 min de lectureSuivantMicroservices Node.js sans dette — REX d'une plateforme fintech multi-canalTech · 16 min de lecture→
Prochain échange

Transformer cette lecture en décision produit.

Si ce sujet ressemble à un problème réel dans votre produit, je peux intervenir sur le diagnostic, l'architecture, le backend, l'interface et les automatisations qui rendent une plateforme exploitable.

Format
CDI, freelance, mission longue
Focus
SaaS, API, back-office, automatisation
Discuter du sujetTélécharger le CV

Newsletter

Recevoir les prochaines notes techniques.

Une sélection courte sur SaaS, architecture backend, automatisation métier et qualité produit. Pas de bruit, seulement des idées applicables.