Convertisseur de devises sur la fiche produit
Ce document présente le convertisseur de devises affiché sur la fiche produit du Store (apps/store), adossé au module CurrencyModule de l’API (apps/api/src/currency). Il permet à un visiteur de visualiser le prix d’un produit — toujours saisi et stocké en FCFA (XAF) — dans quatre autres devises de référence : EUR, USD, CAD, GBP.
1. Vue d’ensemble
Tous les prix du catalogue (Product.price) sont exprimés en XAF : c’est la devise de référence de la marketplace, quel que soit le pays de l’acheteur. Pour autant, un visiteur en zone Euro ou en Amérique du Nord raisonne rarement en FCFA. Le convertisseur de devises répond à ce seul besoin : donner un ordre de grandeur indicatif du prix dans une devise familière, directement sur la fiche produit (apps/store/app/products/[id]/page.tsx → ProductDetailsClient.tsx → CurrencySwitcher.tsx).
[!IMPORTANT] Affichage uniquement — aucun impact sur le panier ni la commande. La conversion est un aparté visuel côté fiche produit. Le panier, le checkout et la commande (
Order) manipulent et enregistrent toujours le montant en XAF (ou dans la devise verrouillée du pays de livraison — voir §4) ; changer la devise affichée sur la fiche produit ne modifie rien côté panier, ne pré-remplit aucun choix de devise de paiement, et ne persiste aucune préférence utilisateur.
2. Calcul des taux (GET /currency/rates)
Le taux n’est ni codé en dur côté frontend, ni entièrement dépendant d’une API tierce : il combine une parité fixe (XAF/EUR) et des taux flottants récupérés en direct (EUR/USD, EUR/GBP, EUR/CAD).
Détails de l’implémentation (currency.service.ts)
- XAF → EUR : dérivé d’une parité fixe par traité,
1 EUR = 655.957 XAF, en vigueur depuis 1999 pour le franc CFA. Cette parité n’étant pas un taux flottant, elle n’est publiée par aucune API de change (BCE, Frankfurter, etc.) — elle est donc codée en constante (EUR_TO_XAF). - EUR → USD / GBP / CAD : récupérés en direct auprès de l’API publique et gratuite Frankfurter (
GET /v1/latest?base=EUR&symbols=USD,GBP,CAD), sans clé d’authentification requise. - XAF → USD / GBP / CAD : calculé par composition,
XAF→EUR × EUR→devise. - Cache en mémoire :
CACHE_TTL_MS = 6h. Un seul appel Frankfurter au plus toutes les 6 heures, indépendamment du nombre de visiteurs. - Résilience : si Frankfurter est injoignable, le service retombe sur le cache existant même expiré (préférable à un taux figé) ; si aucun cache n’existe encore (premier démarrage), il utilise des taux de secours approximatifs codés en dur (
FALLBACK_EUR_RATES) uniquement pour ne jamais casser l’affichage.
[!WARNING] Les taux ne sont pas garantis temps réel. Entre le rafraîchissement Frankfurter (au mieux toutes les 6h) et un éventuel repli sur le cache expiré ou les taux de secours en cas de panne prolongée de l’API tierce, le taux affiché peut être obsolète de plusieurs heures, voire approximatif. C’est acceptable pour un usage indicatif de vitrine, mais ce taux ne doit jamais servir de base à un calcul financier engageant (facturation, remboursement, etc.).
3. Rôles et Autorisations
| Endpoint | Méthode | Rôle requis | Description |
|---|---|---|---|
/currency/rates | GET | Public | Retourne les taux de change courants (base XAF), sans authentification. |
Le contrôleur (currency.controller.ts) est explicitement commenté en ce sens : « le Store doit pouvoir lire les taux sans authentification ». Aucun JwtAuthGuard n’est appliqué.
4. Intégration côté Store
- La page serveur
apps/store/app/products/[id]/page.tsxappelleGET {API_BASE}/currency/ratescôté serveur (cache: 'no-store') lors du rendu de la fiche produit, et transmet le champrates(ounullen cas d’échec réseau) en prop à ProductDetailsClient.tsx. - CurrencySwitcher.tsx (composant client) reçoit
priceXAF(le prix du produit) etrates, et gère uniquement l’état d’affichage (useState<CurrencyCode>('XAF')) :- Un jeu de boutons pastille permet de basculer entre
FCFA / EUR / USD / CAD / GBP. - Le montant converti est calculé côté client (
priceXAF * rates[selected]), sans nouvel appel réseau — les cinq taux sont déjà disponibles en mémoire. - Formatage : FCFA arrondi à l’entier avec séparateurs de milliers (
fr-FR) ; les autres devises affichées avec 4 décimales (compte tenu de taux de change dont la valeur unitaire est parfois très inférieure à 1, ex. XAF→EUR ≈ 0,0015). - Si
ratesestnull(API de taux injoignable au moment du rendu de la fiche), seul le prix en FCFA est affiché et le sélecteur de devises est masqué — aucune conversion approximative n’est proposée sans taux fiable.
- Un jeu de boutons pastille permet de basculer entre
- Un texte de repère (« ≈ montant FCFA au taux du jour ») rappelle systématiquement le prix de référence en FCFA lorsqu’une autre devise est sélectionnée.
[!NOTE] Le module
CurrencyModuleexporteCurrencyService(exports: [CurrencyService], voir currency.module.ts) mais, au moment de la rédaction de ce document, aucun autre module de l’API ne l’injecte : son unique consommateur est le Store via l’endpoint public.
Distinction avec Order.currencyCode
Le schéma Prisma définit par ailleurs Country.currencyCode (devise officielle d’un pays, ex. XAF pour le Cameroun) et Order.currencyCode (@default("XAF")), avec ce commentaire dans schema.prisma :
« Devise et montants figés au moment de la commande (dérivés du pays de l’adresse de livraison à cet instant) — un changement ultérieur des réglages du pays ne doit jamais modifier une commande déjà passée. »
Ce mécanisme est indépendant du convertisseur documenté ici : Order.currencyCode fige la devise réelle de facturation d’une commande à partir du pays de l’adresse de livraison, alors que le CurrencySwitcher de la fiche produit ne fait qu’illustrer un montant XAF dans une autre devise, à titre purement informatif, avant tout ajout au panier. Aucun code observé dans apps/store/app/context/CartContext.tsx ni dans le parcours de checkout ne référence les taux de change : la devise sélectionnée sur la fiche produit ne se propage pas au panier.
5. Guide des Endpoints et Exemples
Lecture des taux (GET /currency/rates)
- Accès : public, aucun header d’authentification requis.
- Réponse (
200 OK) :{ "base": "XAF", "rates": { "XAF": 1, "EUR": 0.0015245, "USD": 0.0016465, "GBP": 0.0012806, "CAD": 0.0022410 }, "fetchedAt": "2026-09-29T08:00:00.000Z" } - Champs :
base: toujours"XAF"— tous les taux sont exprimés comme « 1 XAF = x devise ».rates: facteur multiplicatif à appliquer à un prix XAF pour obtenir le montant dans la devise cible (prixXAF * rates.USD= équivalent en USD).fetchedAt: horodatage ISO 8601 du dernier calcul effectif (peut dater de moins de 6h à cause du cache, ou être plus ancien en cas de repli sur un cache expiré).