Aller au contenu

console-merchant

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

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

Fenêtre de terminal
TITREO_API_BASE_URL=http://localhost:3000
NODE_ENV=development

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

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_session est la credential porteuse vers l’API. Il est posé httpOnly, SameSite=strict et toujours Secure (même en dev sur localhost, qui est un contexte sécurisé) — jamais conditionné à NODE_ENV.
  • middleware.ts protè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-Only par défaut (CSP_REPORT_ONLY=false pour passer en enforce).
  • Chaque appel à l’API porte un X-Correlation-Id tiré par lib/api.ts. En cas d’erreur, ApiHttpError.correlationId reprend 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 /login si 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.

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

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)

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

/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 apiKeyMasked et 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.

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.

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

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.

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.