console-merchant
À quoi ça sert
Section intitulée « À quoi ça sert »console-merchant/ est le back-office self-service du marchand. Une fois intégré à Titreo E-Pay, un marchand doit pouvoir gérer son intégration sans ticket de support : brancher son PSP (Stripe, PayPlug…), enregistrer ses accès Titréo, générer ses clés API, configurer son webhook, suivre ses transactions, rembourser, et inviter son équipe. Cette console est l’interface qui rend tout ça possible.
C’est une brique cliente : elle ne contient aucune logique métier de paiement. Elle est une façade web par-dessus l’API backend. Chaque écran lit ou écrit via les endpoints /v1/merchants/me* et /v1/sessions* du Backend ; l’autorité reste côté API.
Concrètement le marchand y gère lui-même :
| Domaine | Ce qu’il peut faire | Endpoint API sous-jacent |
|---|---|---|
| PSP | Configurer / mettre à jour / supprimer ses clés Stripe, Adyen, PayPlug | /v1/merchants/me/psp-configs[/:provider] |
| Cartes cadeaux | Enregistrer, remplacer, supprimer les accès Titréo de l’enseigne (nom d’enseigne + clé de l’API Titréo REST) | /v1/merchants/me/gift-configs[/:provider] |
| Clés API | Lister, créer une clé (avec scopes) | /v1/merchants/me/api-keys |
| Webhook | URL + secret de notification | /v1/merchants/me (champs webhookUrl, webhookSecret) |
| Livraisons webhook | Lister, inspecter le payload, rejouer (replay) | /v1/merchants/me/webhook-deliveries[/:id][/replay] |
| Transactions | Lister avec filtres (période, statut, « contient »), voir le détail (moyens de paiement, chronologie), exporter en CSV, rembourser par moyen de paiement | /v1/sessions[/:id][/refund][/events], /v1/sessions/export.csv |
| Apparence | Logo + tokens de thème (couleur d’accent…) | /v1/merchants/me/branding |
| Équipe | Inviter, changer de rôle, désactiver un membre | /v1/merchants/me/users[/invites] |
| Compte | Nom, TTL de session | /v1/merchants/me |
Stack et démarrage
Section intitulée « Stack et démarrage »Next.js 15 (App Router, React 19, Server Components + Server Actions), TypeScript strict, Tailwind, validation Zod, lint via Biome. Aucune dépendance HTTP custom : tout passe par fetch côté serveur dans console-merchant/lib/api.ts.
Scripts réels (console-merchant/package.json) :
| Script | Commande | Effet |
|---|---|---|
dev |
next dev -p 3500 |
Serveur de dev sur le port 3500 |
build |
next build |
Build de production |
start |
next start -p 3500 |
Serveur de production sur le port 3500 |
typecheck |
tsc --noEmit |
Vérification de types |
lint |
biome check . |
Lint |
lint:fix |
biome check --write . |
Lint + autofix |
Configuration (console-merchant/.env.example) :
TITREO_API_BASE_URL=http://localhost:3000NODE_ENV=developmentTITREO_API_BASE_URL est l’URL du backend. Lue à un seul endroit (console-merchant/lib/config.ts), valeur par défaut http://localhost:3000. Voir Environnement de dev local.
Deux variables de plus servent la carte de téléchargement de l’étape Installation. Elles sont lues dans console-merchant/lib/downloads.ts et posées par deploy/docker-compose.prod.yml :
| Variable | Rôle | En production |
|---|---|---|
TITREO_CDN_INTERNAL_URL |
Adresse par laquelle le serveur lit downloads/index.json |
http://cdn |
TITREO_CDN_PUBLIC_URL |
Adresse portée par les liens rendus au navigateur | https://cdn.<domaine> |
Sans TITREO_CDN_PUBLIC_URL, ou si l’index ne répond pas en 4 s, aucun paquet n’est proposé : l’étape affiche le guide seul.
Un Dockerfile multi-stage (deps / builder / runner) est présent : il s’appuie sur pnpm + un workspace racine (pnpm-workspace.yaml) et expose le port 3500. Lancement via pnpm start.
Comment l’authentification fonctionne
Section intitulée « Comment l’authentification fonctionne »Le marchand se connecte avec email + mot de passe. La console échange ces identifiants contre un cookie de session opaque qu’elle relaie ensuite à l’API à chaque appel.
sequenceDiagram
participant U as "Navigateur"
participant C as "Console (Server Action)"
participant A as "API Titreo E-Pay"
U->>C: "POST login (email, password)"
C->>A: "POST /v1/auth/login"
A-->>C: "Set-Cookie: tep_session=..."
C->>C: "Pose cookie tep_session (httpOnly, Secure, SameSite=strict)"
C-->>U: "Redirige /onboarding ou /dashboard"
Note over U,A: "Appels suivants : la console relaie tep_session vers l'API"
Points clés du flux d’auth (console-merchant/lib/actions/auth.ts, lib/api.ts, middleware.ts) :
- Le cookie
tep_sessionest la credential porteuse vers l’API. Il est poséhttpOnly,SameSite=strictet toujoursSecure(même en dev surlocalhost, qui est un contexte sécurisé) — jamais conditionné àNODE_ENV. middleware.tsprotège tout sauf les chemins publics (/login,/signup,/verify-email,/accept-invite) : en l’absence du cookie, redirection vers/login.- Le même middleware émet un en-tête CSP (Content-Security-Policy) avec nonce par requête, en mode
Report-Onlypar défaut (CSP_REPORT_ONLY=falsepour passer en enforce). - Chaque appel à l’API porte un
X-Correlation-Idtiré parlib/api.ts. En cas d’erreur,ApiHttpError.correlationIdreprend celui que l’API a renvoyé : c’est la référence à donner au support. Voir Traçabilité. getCurrentUser()(lib/auth.ts) appelle/v1/auth/me;requireMerchant()redirige vers/loginsi l’utilisateur n’est pas un marchand. Voir Sécurité.
Les appels API sont toujours faits côté serveur (Server Actions / RSC). Le navigateur ne parle qu’à la console ; il ne voit jamais l’URL de l’API ni le token en clair côté client.
Structure des routes
Section intitulée « Structure des routes »Le routing App Router se découpe en trois zones, dont deux groupes de routes ((authed), (onboarding)) qui partagent chacun un layout sans apparaître dans l’URL.
app/ page.tsx -> redirige vers /dashboard login/ public signup/ public verify-email/ public (vérification email post-signup) accept-invite/ public (acceptation d'invitation équipe) (authed)/ layout protégé + Sidebar dashboard/ sessions/ [id]/ liste + détail + bouton remboursement psp-configs/ [provider]/ gift-configs/ accès Titréo de l'enseigne api-keys/ webhooks/ deliveries/ [id]/ réglage + livraisons + replay branding/ users/ équipe (invitations, rôles) settings/ (onboarding)/ onboarding/ wizard + layout barre de progression platform/ psp/ gift/ api-key/ webhook/ branding/ plugin/ done/Le (authed)/layout.tsx impose requireMerchant() et affiche le Sidebar. La navigation est filtrée par rôle : chaque entrée du menu porte un scope requis, masqué si le rôle ne le possède pas (components/Sidebar.tsx + lib/permissions.ts).
Rôles et permissions
Section intitulée « Rôles et permissions »Trois rôles marchands, avec des scopes synthétisés côté UI pour masquer/désactiver les contrôles. C’est un miroir de la logique d’autorisation de l’API (api/src/http/plugins/auth.ts:scopesForMerchantRole) ; l’API reste l’autorité, la console ne fait que cacher ce qui serait de toute façon refusé.
| Rôle | Libellé | Périmètre |
|---|---|---|
owner |
Propriétaire | Tout : business + technique |
developer |
Développeur | Sessions, PSP, cartes cadeaux, webhooks, clés API, branding (lecture) |
ops |
Opérations | Sessions (dont remboursement), webhooks, équipe (lecture), branding (lecture) |
Le wizard d’onboarding
Section intitulée « Le wizard d’onboarding »C’est la pièce centrale du parcours marchand. Au premier login, si l’onboarding n’est pas complet (/v1/merchants/me/onboarding renvoie complete: false), la console redirige vers /onboarding au lieu du dashboard.
Les étapes dépendent de la plateforme du marchand, choisie à la première étape. Les accès cartes cadeaux sont requis sur toutes : chaque enseigne appelle Titréo avec les siens.
flowchart LR
subgraph shopify["Shopify"]
direction LR
S1["Boutique"] --> S2["Cartes cadeaux"] --> S3["Clé API"] --> S4["App Shopify"]
end
subgraph autres["PrestaShop et site sur mesure"]
direction LR
P1["Boutique"] --> P2["PSP"] --> P3["Cartes cadeaux"] --> P4["Clé API"] --> P5["Webhook"] --> P6["Apparence (facultative)"] --> P7["Installation"]
end
| Plateforme | Étapes |
|---|---|
| Shopify | platform, gift, api-key, plugin |
| PrestaShop, site sur mesure (JS / API) | platform, psp, gift, api-key, webhook, branding, plugin |
Shopify encaisse lui-même la carte bancaire : l’app ne fait que le miroir des cartes cadeaux, d’où l’absence de PSP et de webhook.
console-merchant/lib/onboarding-steps.ts (stepsFor(platform)) est le miroir exact de api/src/application/merchants/get-onboarding-state.ts (STEPS_BY_PLATFORM) : l’API dit quelles étapes sont faites, la console sait sur quelle page elles se jouent. Modifier l’un sans l’autre désynchronise l’assistant.
La page index (onboarding/page.tsx) interroge l’état et redirige vers la première étape requise non terminée, ce qui permet de reprendre l’onboarding où il s’était arrêté. Sans plateforme choisie, elle mène à /onboarding/platform. Chaque page passe par requireWizardStep (lib/onboarding-guard.ts) : une étape qui n’existe pas pour la plateforme renvoie vers la première étape incomplète, même si l’adresse est tapée à la main.
| Étape | Slug | Ce que fait l’action serveur | État API |
|---|---|---|---|
| Boutique | platform |
PUT /v1/merchants/me (platform : shopify, prestashop ou js) |
platformChosen |
| PSP | psp |
PUT /v1/merchants/me/psp-configs/:provider (clé secrète + secret webhook) |
pspConfigured |
| Cartes cadeaux | gift |
PUT /v1/merchants/me/gift-configs/titreo (nom d’enseigne + clé). L’API vérifie les accès auprès de Titréo avant d’enregistrer |
giftConfigured |
| Clé API | api-key |
POST /v1/merchants/me/api-keys (scopes sessions:read/write, préfixe tep_live_) — le token n’est montré qu’une fois |
apiKeyCreated |
| Webhook | webhook |
PUT /v1/merchants/me (webhookUrl + webhookSecret) |
webhookConfigured |
| Apparence | branding |
PUT /v1/merchants/me/branding (logo, token --accent). Facultative : elle ne bloque pas complete |
brandingSet |
| Installation | plugin |
Aucune écriture : guide d’installation et paquet à télécharger | pluginInstalled |
giftConfigured exige des accès actifs et vérifiés (verifiedAt non nul). pluginInstalled se valide seule : elle passe à fait dès qu’une clé API du marchand a été utilisée, et pour PrestaShop et JS que le webhook est configuré.
L’étape Cartes cadeaux
Section intitulée « L’étape Cartes cadeaux »/onboarding/gift et la page /gift-configs partagent le formulaire components/GiftConfigForm.tsx et les actions de lib/actions/gift-configs.ts. Le marchand saisit le nom de son enseigne et sa clé de l’API Titréo REST.
- Titréo refuse les accès → l’API répond
422 GIFT_CREDENTIALS_REJECTED, rien n’est enregistré. - Titréo est injoignable →
503 GIFT_PROVIDER_UNREACHABLE, rien n’est enregistré, les accès précédents restent en place. - La clé n’est jamais réaffichée : la page montre
apiKeyMaskedet la date de dernière vérification.
Supprimer les accès retire les cartes cadeaux de la page de paiement. Les cartes déjà débitées ne peuvent plus être remboursées tant que des accès ne sont pas enregistrés de nouveau.
L’étape Installation
Section intitulée « L’étape Installation »onboarding/plugin/page.tsx affiche le guide de la plateforme et, quand un paquet est publié, une carte de téléchargement : nom, version, prérequis, taille, lien, empreinte SHA-256 à comparer (shasum -a 256 <fichier>).
| Plateforme | Paquet proposé |
|---|---|
| PrestaShop | Le module (presta), dernière version publiée |
| Site sur mesure | Le SDK JavaScript (sdk-js), avec la commande npm install <adresse du fichier> |
| Shopify | Aucun : l’app est un service hébergé, installé par son lien d’installation |
Les paquets viennent de downloads/index.json, écrit par tep plugin publish. Voir Plugin PrestaShop.
Un badge dit si l’installation est constatée : « Première requête reçue » dès que la clé API a servi, « En attente » sinon.
Esquisse vs réel
Section intitulée « Esquisse vs réel »Pour une passation honnête, voici la distinction sans fard.
Réel et branché sur l’API :
- Auth complète : login, signup (avec lien de vérification), vérification email, accept-invite, logout — toutes via
/v1/auth/*. - CRUD PSP, accès cartes cadeaux, clés API, webhook, branding, équipe (invitations / rôles / désactivation) — vrais endpoints
/v1/merchants/me*. - Transactions : liste paginée par curseur avec, par ligne, le n° de commande (
metadata.order), le total, ce qu’ont payé les cartes cadeaux (montant + émetteur) et la carte bancaire (marque •••• 4 derniers · wallet + montant), le remboursé ; export CSV (/sessions/export, relais Next vers/v1/sessions/export.csv, formats Excel FR et standard) ; détail avec tableau des moyens de paiement et chronologie (/v1/sessions/:id/events) ; remboursement par moyen de paiement (POST /v1/sessions/:id/refund, voir ci-dessous) et garde de statut remboursable (lib/sessions-helpers.ts). - Livraisons webhook : liste, détail du payload, replay (
POST .../replay). - Wizard d’onboarding piloté par l’état réel renvoyé par l’API.
- Garde de sécurité côté serveur sur le branding : ré-validation Zod (tokens CSS, logo) car les Server Actions sont des POST directement appelables (
lib/actions/branding.ts).
Esquisse / non finalisé :
- Aucun test (ni unitaire ni e2e).
- Design system réduit à
components/ui.tsx; pas de bundle design intégré. - Pas de gestion d’erreur réseau riche (états de chargement / retry minimalistes).
Remboursement par moyen de paiement
Section intitulée « Remboursement par moyen de paiement »app/(authed)/sessions/refund-button.tsx propose une case à cocher par moyen de paiement encaissé : « Carte cadeau Illicado •••• 1234 », « Carte bancaire Visa •••• 4242 », avec son montant.
- Chaque moyen coché est remboursé en entier, sur son support d’origine. Il n’y a pas de saisie de montant.
- Les moyens non cochés restent encaissés et pourront être remboursés plus tard.
- Toutes les cases sont cochées à l’ouverture. Le bouton affiche le total et reste inactif sans case cochée.
- L’action serveur (
refundSessionAction,lib/actions/sessions.ts) envoie{ legIds, reason? }. Sans sélection elle refuse : l’API, elle, rembourserait tout. - Au retour, elle compare
refundedLegIdsà la sélection. S’ils diffèrent, elle affiche une erreur plutôt qu’un succès.
Les anciennes stratégies gift_first et cb_first n’existent plus : l’API les refuse en 400. Voir Couche HTTP.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Backend — la source des endpoints
/v1/merchants/me*et de l’autorisation par scopes. - Contrats inter-briques — pourquoi la console est une façade jetable.
- Sécurité — cookie de session, CSP, frontière de confiance des Server Actions.
- Environnement de dev local — lancer la console (port 3500) contre l’API (port 3000).
- Vue d’ensemble de l’écosystème — où se place cette brique.
Apparence (v2, 28/08/2026)
Section intitulée « Apparence (v2, 28/08/2026) »Identité BH Technologies, sobre : blanc en principal, noir pour le texte et les actions (.btn-primary), orange #f26522 réservé à l’accent — marque, entrée de navigation active, focus, chiffre clé, encart d’onboarding ; dégradés et transparences n’existent que sur cet accent. Tout vient de app/tokens.css (clair et sombre) et des classes de app/globals.css (.btn, .input, .select, .chip, .card, .callout, .stat), que les primitives de components/ui.tsx reprennent. components/Brand.tsx porte la marque, components/NavItem.tsx (client) l’état actif. Aucune couleur n’est écrite dans une page.