Aller au contenu

Backend api/ — vue d'ensemble & arborescence

Le dossier api/ est le cœur de Titreo E-Pay (nom interne OrchPay). C’est le service Node/Fastify qui orchestre un paiement composé de plusieurs instruments : une ou plusieurs cartes cadeaux (via Titreo) plus un moyen de paiement classique (PSP : Stripe, Adyen…). Tout le reste de l’écosystème — SDK client, plugins CMS, console marchand — n’est qu’un client de l’API publique exposée ici.

Concrètement, le backend :

  • expose une API REST (sessions de paiement, legs, manifest de bootstrap, webhooks PSP, administration marchand) ;
  • maintient l’état d’une PaymentSession et de ses PaymentLeg via des machines à états strictes (voir Machines à états) ;
  • garantit la cohérence des opérations multi-instruments grâce à un pattern outbox traité par un worker séparé (voir Outbox & orchestration) ;
  • chiffre les secrets PSP, signe les manifests, hash les mots de passe, et applique l’idempotence sur les opérations externes.

L’ensemble suit une architecture hexagonale : le métier ne connaît ni Fastify, ni Firebird, ni Stripe. Il dialogue uniquement avec des interfaces (« ports »), dont les implémentations concrètes (« adapters ») vivent en périphérie.

api/src/
├── domain/ couche 1 — métier pur, zéro import framework/infra
│ ├── session/ PaymentSession, PaymentLeg + leurs machines à états
│ ├── merchant/ règles marchand (branding…)
│ ├── auth/ scopes & utilisateurs (logique d'autorisation pure)
│ ├── bootstrap/ construction/validation du manifest SDK
│ └── shared/ Money, Currency, Id, erreurs (DomainError…)
├── application/ couche 2 — use cases + ports (interfaces)
│ ├── ports/ interfaces des dépendances externes (repos, registries…)
│ ├── sessions/ create / add-gift-leg / set-psp-leg / submit / refund…
│ ├── gift/ débit / remboursement / lookup solde carte cadeau
│ ├── webhooks/ handle-psp-webhook, dispatch webhook marchand
│ ├── workers/ process-outbox-entry, cleanup-expired-sessions
│ ├── bootstrap/ generate-manifest
│ ├── auth/ signup / login / logout / verify-email
│ ├── merchants/ CRUD marchand + onboarding
│ ├── merchant-users/ invitations & membres d'équipe
│ ├── api-keys/ création / révocation / authentification clés API
│ ├── psp-configs/ upsert / get / list / delete config PSP
│ ├── branding/ get / update branding
│ └── audit/ write / list journaux d'audit
├── infrastructure/ couche 3 — implémentations concrètes des ports
│ ├── db/ Firebird : pool, repos, UnitOfWork, migrations SQL
│ ├── psp/ adapters Stripe & Adyen, registry, validateurs de config
│ ├── titreo/ adapter carte cadeau Titreo (réel)
│ ├── gift/ adapter carte cadeau mock (dev/test)
│ ├── gift-card/ registry de fournisseurs de cartes cadeaux
│ ├── crypto/ AEAD, Ed25519, scrypt, hash clé API, tokens session
│ ├── bootstrap/ registry des client-adapters (manifest)
│ ├── email/ ConsoleEmailSender (dev)
│ ├── webhooks/ HttpWebhookSender + url-guard (anti-SSRF)
│ ├── workers/ OutboxPoller, cleanup-scheduler (mécanique de polling)
│ ├── clock.ts SystemClock
│ └── id-generator.ts UuidGenerator
├── http/ couche 4 — Fastify (adapter d'entrée, jetable)
│ ├── server.ts point d'entrée : charge config + assemble le container
│ ├── app.ts buildHttpApp() : registre plugins + routes
│ ├── routes/ un fichier par groupe d'endpoints (sessions, auth, admin…)
│ ├── plugins/ auth, error-handler, request-context, idempotency, audit…
│ ├── schemas/ schémas Zod partagés (sessions)
│ └── serializers.ts sérialisation domaine → JSON de sortie
├── workers/ couche 5 — points d'entrée des processus séparés
│ ├── outbox.ts worker outbox (confirme gifts après capture PSP)
│ └── cleanup.ts worker d'expiration des sessions en attente
├── config/ lecture + validation Zod des variables d'env
│ └── index.ts loadConfig()
└── container.ts COMPOSITION ROOT — câble ports ↔ adapters ↔ use cases
Dossier Couche Rôle Dépend de
domain/ 1 — Domain Entités, machines à états, Money/Currency, erreurs métier. Pur. lui-même + zod
application/ 2 — Application Use cases (un cas d’usage = une classe) + ports/ (interfaces). domain/ + ports/
application/ports/ 2 — Application Contrats des dépendances externes (repos, registries, clock, signer…). domain/
infrastructure/ 3 — Infrastructure Implémentations concrètes : Firebird, Stripe/Adyen, Titreo, crypto, webhooks. domain/, application/ports, libs externes
http/ 4 — HTTP Serveur Fastify, routes, plugins, validation Zod. Aucune logique métier. application (use cases) + container
workers/ 5 — Workers Entry points de processus détachés (outbox, cleanup). application + infrastructure (composition)
config/ transverse loadConfig() lit process.env, valide via Zod. Seul endroit qui lit l’env. zod
container.ts composition root Instancie les adapters et injecte tout dans les use cases. tout (sauf domain directement)

Pour le détail de chaque couche, voir Couche Domain, Couche Application, L’orchestrateur, Couche Infrastructure, Couche HTTP, Workers et Base de données.

Une seule règle gouverne l’architecture : les flèches de dépendance pointent vers l’intérieur. Le métier ne dépend de rien ; tout dépend (indirectement) du métier.

flowchart TD
    subgraph "Périphérie (jetable)"
        HTTP["http/ (Fastify, routes)"]
        WORK["workers/ (processus séparés)"]
        INFRA["infrastructure/ (Firebird, Stripe, Titreo, crypto)"]
    end
    subgraph "Cœur (stable)"
        APP["application/ (use cases + ports)"]
        DOM["domain/ (entités, états, Money)"]
    end
    HTTP --> APP
    WORK --> APP
    WORK --> INFRA
    INFRA --> APP
    APP --> DOM
    INFRA -.implémente.-> APP

Lecture : http/ et workers/ appellent des use cases de application/. infrastructure/ implémente les ports de application/. application/ ne connaît que domain/ et ses propres interfaces. domain/ ne connaît personne.

Interdits absolus (vérifiés à la revue) :

  • domain/ n’importe jamais fastify, firebird, stripe, bullmq, etc. ;
  • application/ n’importe jamais une implémentation concrète d’infrastructure — uniquement les ports ;
  • http/ ne contient jamais de logique métier (une route = parse + appel use case + sérialise).

Le détail conceptuel et les bénéfices (migration Fastify → NestJS sans réécriture du métier) sont dans Architecture hexagonale.

container.ts est l’unique endroit où le métier abstrait rencontre le monde concret. Il ne contient aucune logique métier : il se contente d’instancier chaque use case en lui injectant ses dépendances (par constructeur, pas de singleton global, pas de conteneur DI magique).

Sa surface publique :

  • le type Container — la liste exhaustive de ce que le reste de l’app peut consommer : les ports déjà résolus (sessions, outbox, merchants, pspRegistry, giftCardRegistry, clock…) et un sous-objet useCases (une cinquantaine de use cases : createSession, submitSession, refundSession, handlePspWebhook, processOutboxEntry, generateManifest, loginMerchant…) ;
  • le type ContainerDeps — les adapters bruts à fournir en entrée (un par port + consoleBaseUrl) ;
  • la fonction buildContainer(deps: ContainerDeps): Container — qui câble tout.

Chemin d’appel typique au démarrage du serveur HTTP (http/server.ts) :

// 1. lire + valider l'environnement
const config = loadConfig()
// 2. instancier les adapters concrets (infrastructure)
const db = await FirebirdDb.connect(config.firebird)
const aead = new AesGcmAead(loadKekFromBase64(config.pspConfigKek))
// 3. câbler tous les use cases via le composition root
const container = buildContainer({
clock: new SystemClock(),
ids: new UuidGenerator(),
sessions: new FirebirdSessionRepo(db, aead),
pspRegistry: new ConfigBackedPspRegistry(factories, pspConfigs),
giftCardRegistry, // Titreo réel si config.titreo, sinon MockGiftCardAdapter
/* …une vingtaine d'autres ports… */
})
// 4. monter Fastify par-dessus le container
const app = await buildHttpApp({ container, /* secrets, CORS, rate-limit… */ })
await app.listen({ port: config.port, host: config.host })

Le worker outbox (workers/outbox.ts) suit le même schéma de câblage, puis branche un OutboxPoller sur container.useCases.processOutboxEntry au lieu de monter un serveur HTTP.

Le document de cadrage ARCHITECTURE.md (à la racine app/, pas dans api/) décrit une cible qui a divergé de l’implémentation. Points à connaître pour ne pas chercher des fichiers inexistants :

Pour comprendre… Page
Le métier pur (entités, Money, erreurs) Couche Domain
Les use cases et les ports Couche Application
Le séquencement paiement + outbox L’orchestrateur
Les adapters concrets (Firebird, PSP, Titreo, crypto) Couche Infrastructure
Le serveur Fastify, routes & plugins Couche HTTP
Les processus détachés (outbox, cleanup) Workers
Le schéma SQL et les migrations Base de données
Le trajet complet d’une requête Parcours d’appel

Côté concepts transverses : Machines à états, Outbox & orchestration, Idempotence, Contrats, Sécurité.