Cahier de Recette — QA Master
Version 1.1 · Mai 2026 · Confidentiel — cliquer sur un statut pour le modifier
v1.0 Prod NestJS 10 · Next.js 15 PostgreSQL 16 · Redis 7 Mobile Money · Wave · Orange Money ISO 8583 · EMV · HMAC-SHA256
Tableau de bord recette
0
Total
0
Validés
0
En échec
0
Bloquants
0
A tester
0
N/A
Progression recette 0 %
■ PASS ■ FAIL ■ BLOQUANT ■ N/A
Tests P0 obligatoires avant mise en production : anti double-dépense (POS-03), invariant ledger débit=crédit (LED-01 à LED-05), webhook HMAC Wave/OM (PAY-02/PAY-04), OTP blocage 3 échecs (AUTH-03), import CSV 500 lignes (RH-03), parcours E2E complets (E2E-01 à E2E-04).
Environnement cible : tester sur Android bas de gamme (Tecno Y4) + réseau 3G intermittent. Tous les montants sont en centimes FCFA (ex. 10 000 FCFA = 1 000 000 centimes).
Couverture par module
ModuleNb casP0P1P2
Authentification (OTP / JWT)10343
Bénéficiaire — Wallet & QR12453
RH Entreprise — Émission & Dashboard14374
POS Commerçant — Scan & Validation12552
Paiements Mobile Money & Webhooks10442
Sécurité & Anti-fraude8431
Ledger & Comptabilité8431
Parcours E2E complets4400
TOTAL78313116
Légende des statuts — cliquer pour filtrer
Cliquer sur un badge de statut dans n'importe quel tableau pour le faire passer à l'état suivant. Les statuts sont sauvegardés localement.

P0 Critique — obligatoire avant prod    P1 Haute priorité    P2 Normale    P3 Basse
Les statuts sont sauvegardés dans le navigateur (localStorage).
Authentification — OTP / JWT RS256
Règles critiques : OTP 6 chiffres · TTL Redis 5 min · max 3 envois/heure/numéro · blocage 30 min après 3 échecs · JWT Access 15 min · Refresh 30 jours · rotation stricte de famille.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
AUTH-01
Envoi OTP valide
Numéro +221 E.164 valide
P1 Numéro inexistant en DB
  1. POST /auth/otp/send { phone: "+221771234567" }
  2. Vérifier SMS reçu
HTTP 200 · SMS reçu avec code 6 chiffres · clé Redis otp:+221771234567 TTL 300s
AUTH-02
Vérification OTP correct
P1 OTP envoyé (AUTH-01)
  1. POST /auth/otp/verify { phone, code }
  2. Vérifier réponse
HTTP 200 · accessToken (JWT RS256, exp 15 min) · refreshToken (30 jours) · User créé si nouveau
AUTH-03
OTP incorrect — compteur d'échecs
P0 OTP envoyé
  1. POST /auth/otp/verify avec mauvais code × 3
  2. 4ème tentative
3 premiers : HTTP 401 INVALID_OTP · 4ème : HTTP 429 · clé otp_blocked TTL 1800s créée en Redis
AUTH-04
OTP expiré après 5 min
P0 OTP envoyé
  1. Attendre 6 min (ou forcer TTL Redis à 0)
  2. POST /auth/otp/verify avec bon code
HTTP 401 · message "Code expiré"
AUTH-05
Limite d'envoi OTP (3/heure)
P0 Numéro non bloqué
  1. Envoyer OTP 4 fois en moins d'1h sur le même numéro
3 premiers : HTTP 200 · 4ème : HTTP 429 RATE_LIMIT_EXCEEDED
AUTH-06
Refresh token — rotation stricte
P1 Session active
  1. POST /auth/refresh avec refreshToken valide
  2. Réutiliser l'ancien refreshToken
Étape 1 : nouveaux access + refresh tokens · Étape 2 : HTTP 401 (ancien token invalidé — rotation stricte)
AUTH-07
Accès route protégée sans token
P1
  1. GET /api/vouchers/me sans header Authorization
HTTP 401 Unauthorized
AUTH-08
RBAC — mauvais rôle
P1 Token BENEFICIARY valide
  1. GET /api/admin/dashboard avec token BENEFICIARY
HTTP 403 Forbidden
AUTH-09
Numéro non E.164 Sénégal
P2
  1. POST /auth/otp/send { phone: "0612345678" }
HTTP 400 · message "Numéro invalide — format +221XXXXXXXXX requis"
AUTH-10
Déconnexion — blacklist token Redis
P2 Session active
  1. POST /auth/logout
  2. Réutiliser l'accessToken
Étape 1 : HTTP 200 · Étape 2 : HTTP 401 (token en blacklist Redis)
Espace Bénéficiaire — Wallet & QR
Routes : /app/wallet · /app/wallet/[voucherId] — Affichage montants : toujours centimes / 100. Screen Wake Lock actif sur QR plein écran. Service Worker cache les QR actifs.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
BEN-01
Affichage wallet — liste des bons
P1 Bénéficiaire avec 2 bons ISSUED
  1. Ouvrir /app/wallet
  2. Vérifier la liste
2 bons affichés · montants en FCFA (centimes/100) · statuts corrects
BEN-02
QR code plein écran
P0 Bon ISSUED actif
  1. Cliquer sur un bon
  2. Vérifier /app/wallet/[voucherId]
QR affiché plein écran · Screen Wake Lock actif · données QR : JSON signé HMAC-SHA256
BEN-03
QR offline — Service Worker
P0 QR précédemment affiché (SW actif)
  1. Mettre en mode avion
  2. Ouvrir /app/wallet/[voucherId]
QR affiché depuis cache SW · pas d'erreur réseau visible
BEN-04
Bon expiré — affichage statut
P1 Bon avec expiresAt dépassé
  1. Ouvrir /app/wallet
  2. Vérifier le bon expiré
Badge EXPIRÉ · pas de lien vers QR · montant barré
BEN-05
Bon partiellement utilisé
P1 Bon en statut PARTIAL
  1. Ouvrir le bon PARTIAL
Solde restant = remainingValue/100 FCFA · statut PARTIAL · QR accessible
BEN-06
Onglet Offres — chargement
P1 Session active
  1. Cliquer onglet "Offres"
  2. Attendre le chargement
Liste des offres affichée · pas d'écran blanc · pas d'erreur 401 silencieuse
BEN-07
Onglet Fidélité — chargement
P1 Session active
  1. Cliquer onglet "Fidélité"
Données de fidélité affichées · si 401 : message "Session expirée" + bouton reconnexion visible
BEN-08
Affichage montant — conversion centimes
P0 Bon nominalValue = 5 000 000 centimes
  1. Ouvrir le bon
  2. Lire le montant affiché
Affiché : "50 000 FCFA" (5 000 000 / 100) · jamais "5000000" brut
BEN-09
PWA — installation home screen
P2 Android Chrome
  1. Ouvrir kado.sn sur Chrome Android
  2. Installer l'application
Icône sur home screen · thème #534AB7 · start_url = /app/wallet · mode standalone
BEN-10
Bon PENDING — QR non accessible
P2 Bon créé non encore confirmé (emeConfirmedAt = null)
  1. Voir le bon PENDING dans le wallet
Badge "En attente" · QR non accessible · message explicatif affiché
BEN-11
Bon scheduledAt futur
P2 Bon avec scheduledAt dans 3 jours
  1. Ouvrir le wallet
Date d'activation future visible · QR non accessible avant cette date
BEN-12
Performances 3G — chargement wallet
P1 Réseau 3G simulé (throttling)
  1. Ouvrir /app/wallet en 3G
  2. Mesurer temps de chargement
Wallet visible en moins de 4s · skeletons affichés pendant le chargement
Espace RH Entreprise — Émission & Dashboard
Routes : /dashboard · Rôles : COMPANY_ADMIN, COMPANY_VIEWER. Import CSV max 500 lignes. Bon expiresAt défaut J+180. Montants toujours en centimes.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
RH-01
Émission bon unitaire
P0 COMPANY_ADMIN · provision > montant
  1. POST /api/vouchers { beneficiaryPhone, amount (centimes), type }
  2. Vérifier DB + ledger
Voucher PENDING créé · LedgerEntry ISSUE : PROVISION_COMPANY → VOUCHER_LIABILITY · SMS envoyé
RH-02
Émission — provision insuffisante
P0 Provision = 0
  1. Tenter d'émettre un bon
HTTP 409 INSUFFICIENT_PROVISION · aucun bon créé · ledger non touché
RH-03
Import CSV — 500 collaborateurs
P0 CSV valide 500 lignes · provision suffisante
  1. Uploader le CSV via /dashboard/import
  2. Attendre le traitement
500 bons créés · rapport d'erreurs si lignes invalides · aucun doublon · SMS en queue Bull asynchrone
RH-04
Import CSV — lignes invalides
P1 CSV avec 5 lignes invalides
  1. Uploader le CSV
Lignes valides importées · rapport d'erreurs par ligne (numéro, motif) · traitement non bloqué
RH-05
Annulation bon ISSUED → remboursement provision
P1 Bon ISSUED non utilisé
  1. PATCH /api/vouchers/:id/cancel
  2. Vérifier ledger et provision
Voucher → CANCELLED · LedgerEntry CANCEL : VOUCHER_LIABILITY → PROVISION_COMPANY · provision restituée
RH-06
Annulation bon USED — impossible
P1 Bon USED
  1. Tenter d'annuler le bon USED
HTTP 409 · message "Bon déjà utilisé — annulation impossible"
RH-07
Onboarding — invitation collaborateur
P1 COMPANY_ADMIN connecté
  1. POST /api/invitations { email/phone, firstName, poste }
  2. Vérifier token UUID + TTL 7 jours
Invitation créée · token unique · SMS/email envoyé · User créé en DB lors de l'acceptation
RH-08
Dashboard — statistiques temps réel
P2 Entreprise avec bons en circulation
  1. Ouvrir /dashboard
  2. Vérifier les chiffres
Total émis, total utilisé, solde provision, nb bons actifs — tous cohérents avec la DB
RH-09
COMPANY_VIEWER — lecture seule
P2 Rôle COMPANY_VIEWER
  1. Tenter d'émettre un bon avec un token VIEWER
HTTP 403 · Dashboard lisible · aucune action de mutation disponible
RH-10
Émission planifiée (scheduledAt)
P2 scheduledAt dans 7 jours
  1. Émettre bon avec scheduledAt futur
  2. Vérifier statut avant et après la date
Avant date : Voucher PENDING · SMS non envoyé · Après date : ISSUED + SMS envoyé via cron
RH-11
Isolation entreprises — companyId
P1 2 entreprises A et B
  1. COMPANY_ADMIN de A tente GET /api/vouchers?companyId=[B]
HTTP 403 ou résultat vide · aucun bon de B visible par A
RH-12
Note RH — max 200 caractères
P2
  1. Émettre avec note de 201 caractères
HTTP 400 · validation "note max 200 caractères"
RH-13
Cron expiration bons — 00h01 UTC
P1 Bons ISSUED avec expiresAt = hier
  1. Déclencher manuellement le cron d'expiration
  2. Vérifier DB et ledger
Bons → EXPIRED · LedgerEntry EXPIRE : VOUCHER_LIABILITY → EXPIRED_FORFEIT
RH-14
Export liste bons — CSV
P2 Entreprise avec 20 bons
  1. GET /api/vouchers/export?format=csv
Fichier CSV téléchargé · colonnes : id, beneficiaryPhone, amount (FCFA), status, expiresAt
POS Commerçant — Scan & Validation QR
Règle critique P0 : la validation s'exécute dans une transaction Prisma atomique avec SELECT FOR UPDATE. Zéro double-dépense tolérée.
Routes : /pos/scan · /pos/amount · /pos/confirm. Caméra active immédiatement. Pas de bouton "Rendu monnaie". Montant max = solde disponible.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
POS-01
Scan QR valide — validation partielle
P0 Bon ISSUED · solde 50 000 FCFA · commerçant actif compatible
  1. Scanner le QR sur /pos/scan
  2. Saisir 20 000 FCFA
  3. Confirmer
Bon → PARTIAL · remainingValue = 3 000 000 centimes · vibration 200ms · fond vert 2s · son validation
POS-02
Scan QR valide — utilisation totale
P0 Bon ISSUED · solde exact = montant saisi
  1. Scanner QR · saisir le montant exact · confirmer
Bon → USED · remainingValue = 0 · LedgerEntry REDEEM · feedback succès
POS-03
Anti double-dépense — concurrence
P0 Même bon ISSUED · 2 POS simultanés
  1. Déclencher 2 validations simultanées sur le même bon (Promise.all)
Exactement 1 succès (HTTP 200) · 1 rejet (HTTP 409) · SELECT FOR UPDATE garantit l'atomicité
POS-04
QR signature HMAC invalide
P0 QR falsifié (signature modifiée)
  1. POST /api/vouchers/validate avec qrData à signature invalide
HTTP 400 QR_INVALID · comparaison timingSafeEqual · aucune modification en DB
POS-05
Montant > solde disponible
P0 Bon PARTIAL · solde 5 000 FCFA
  1. Saisir 10 000 FCFA sur /pos/amount
Champ bloqué au-delà du solde · HTTP 409 INSUFFICIENT_BALANCE via API · aucun bouton "Rendu monnaie"
POS-06
Bon expiré — refus de validation
P1 Bon avec expiresAt dépassé
  1. Scanner le QR du bon expiré
HTTP 409 VOUCHER_EXPIRED · message clair sur l'écran POS
POS-07
Type bon incompatible commerçant
P1 Bon TRANSPORT chez commerçant FOOD
  1. Scanner le QR et tenter la validation
HTTP 409 TYPE_NOT_ALLOWED · message "Ce bon n'est pas accepté dans cette enseigne"
POS-08
Caméra — activation immédiate
P1 Android Chrome avec caméra
  1. Ouvrir /pos/scan
  2. Mesurer délai d'activation
Caméra active en moins de 2s · pas de menu intermédiaire
POS-09
Commission kado 5%
P1 Bon ISSUED · montant 10 000 FCFA
  1. Valider 10 000 FCFA
  2. Vérifier le ledger
MERCHANT_PAYABLE = 9 800 FCFA · REVENUE_COMMISSION = 200 FCFA · Math.round (pas de float)
POS-10
Idempotence validation
P1 Même reference idempotence
  1. POST /api/vouchers/validate 2 fois avec même reference UUID
2ème appel : HTTP 409 DUPLICATE_TRANSACTION · aucune nouvelle écriture ledger
POS-11
Commerçant SUSPENDED
P2 Commerçant statut SUSPENDED
  1. Tenter de valider avec token MERCHANT SUSPENDED
HTTP 403 · message "Compte commerçant suspendu"
POS-12
Timeout transaction Prisma (5s)
P2 DB simulée lente (> 5s)
  1. Simuler une DB lente lors d'une validation
Transaction annulée après 5s · aucune écriture partielle · bon dans état initial
Paiements Mobile Money — Webhooks
Règle critique P0 : vérification HMAC-SHA256 obligatoire AVANT tout parsing JSON. timingSafeEqual obligatoire. Signature invalide → 401 sans traitement.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
PAY-01
Webhook Wave — signature valide
P0 Secret Wave configuré en ENV
  1. POST /api/webhooks/wave avec body signé correctement
HTTP 200 · bon passé PENDING → ISSUED (emeConfirmedAt renseigné)
PAY-02
Webhook Wave — signature invalide
P0
  1. POST /api/webhooks/wave avec x-wave-signature falsifiée
HTTP 401 · aucun traitement · événement logué comme tentative invalide
PAY-03
Webhook Orange Money — signature valide
P0 Secret OM configuré
  1. POST /api/webhooks/orange-money avec body signé
HTTP 200 · bon activé
PAY-04
Webhook — rawBody disponible
P0
  1. POST webhook sans rawBody (body déjà parsé par middleware)
HTTP 400 "rawBody non disponible" · pas de crash silencieux
PAY-05
Provision entreprise via Mobile Money (Bictorys)
P1 Entreprise active
  1. POST /api/payments/topup { companyId, amount }
  2. Simuler confirmation webhook Bictorys
Checkout créé · après confirmation webhook : PROVISION_COMPANY crédité · LedgerEntry PROVISION (CASH_BICTORYS → PROVISION_COMPANY)
PAY-06
Reversement commerçant T+1
P1 MERCHANT_PAYABLE > 0
  1. Déclencher le cron de reversement (23h00 UTC)
  2. Vérifier ledger
LedgerEntry SETTLE : MERCHANT_PAYABLE → MERCHANT_SETTLED · référence idempotence unique
PAY-07
Retry reversement — 3 tentatives exponential backoff
P1 API Mobile Money indisponible
  1. Simuler erreur API · déclencher le job
3 tentatives · backoff exponentiel (delay 60s) · après 3 échecs : job "failed" · alerte loguée
PAY-08
Idempotence reversement
P1 Reversement déjà effectué
  1. Déclencher 2 fois le cron pour la même date et commerçant
2ème exécution : contrainte UNIQUE sur MerchantSettlement.reference → pas de double versement
PAY-09
Webhook — doublon d'événement
P2 Même webhook reçu 2 fois
  1. Rejouer le même webhook Mobile Money (même ID d'événement)
2ème appel : idempotent (HTTP 200) ou HTTP 409 · aucune double activation de bon
PAY-10
Queue Bull — appels synchrones bannis
P2
  1. Audit du code : vérifier l'absence d'appels directs vers Mobile Money hors de la queue Bull
Tous les appels passent par la queue Bull (via Bictorys) · aucun appel synchrone dans les controllers
Sécurité & Anti-fraude
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
SEC-01
Rate limiter global — 100 req/min IP
P0
  1. Envoyer 101 requêtes en 1 min depuis la même IP
101ème requête : HTTP 429 RATE_LIMIT_EXCEEDED
SEC-02
Rate limiter /auth/otp — 30 req/min
P0
  1. Envoyer 31 requêtes OTP en 1 min
31ème requête : HTTP 429
SEC-03
Détection fraude — validation au centime exact
P0 Commerçant avec 10 transactions au centime exact
  1. Créer 10 validations successives au montant exact du bon
  2. Vérifier le backoffice
Alerte générée dans le backoffice · commerçant marqué pour revue
SEC-04
Détection fraude — pic volume +300%
P0 Commerçant avec historique 4 semaines
  1. Déclencher un volume 4× la moyenne sur 1 journée
Alerte générée · commerçant marqué pour revue manuelle
SEC-05
Même bénéficiaire > 3x/jour chez même commerçant
P1
  1. Valider 4 bons du même bénéficiaire chez le même commerçant en 1 journée
4ème validation : blocage automatique · HTTP 409 ou alerte backoffice
SEC-06
Secrets — aucun hardcode dans le code
P1
  1. Audit grep : WAVE_API_KEY, JWT_PRIVATE_KEY, HMAC_VOUCHER_SECRET
Zéro occurrence de secrets en dur · tous via process.env
SEC-07
TypeScript strict — zéro any implicite
P1
  1. Lancer npm run typecheck
0 erreur TS · "strict": true dans tsconfig · aucun any implicite
SEC-08
CORS — origines non autorisées
P2 ALLOWED_ORIGINS configuré
  1. Requête depuis https://attacker.com vers /api/*
HTTP 403 · header CORS absent ou restreint
Ledger & Comptabilité — Invariants
Invariant absolu : SUM(débit) = SUM(crédit) pour chaque opération. INSERT ONLY — aucun UPDATE ni DELETE sur LedgerEntry. Montants toujours en centimes (Int), jamais Float.
IDCas de testPrioPréconditionsÉtapesRésultat attenduStatut
LED-01
Invariant débit = crédit — émission
P0 Bon ISSUED
  1. Émettre un bon de 50 000 FCFA
  2. SELECT SUM(amount) GROUP BY side FROM LedgerEntry WHERE voucherId = :id
SUM(débit) = SUM(crédit) = 5 000 000 centimes
LED-02
Invariant débit = crédit — validation POS
P0 Bon validé en POS
  1. Valider 10 000 FCFA
  2. Vérifier entrées ledger (REDEEM + COMMISSION)
VOUCHER_LIABILITY → MERCHANT_PAYABLE (9 800 FCFA) + REVENUE_COMMISSION (200 FCFA) · débit = crédit = 10 000 FCFA
LED-03
INSERT ONLY — tentative d'UPDATE
P0 LedgerEntry existante
  1. Tenter UPDATE sur une LedgerEntry via Prisma
Erreur runtime ou contrainte DB · aucune modification acceptée
LED-04
Reference idempotence — unicité UNIQUE
P0 LedgerEntry avec reference = "abc123"
  1. Tenter d'insérer une 2ème LedgerEntry avec la même reference
Contrainte UNIQUE violée · erreur Prisma · aucune entrée dupliquée
LED-05
Montants en centimes — jamais Float
P0
  1. SELECT amount FROM LedgerEntry WHERE amount != ROUND(amount)
  2. Audit TS : chercher parseFloat() sur des montants
0 résultat SQL · 0 occurrence Float/parseFloat sur montants
LED-06
Commission — Math.round (pas de float)
P1 Montant 33 333 FCFA (montant impair)
  1. Valider 3 333 300 centimes
  2. Vérifier commission = Math.round(3333300 * 0.05)
Commission = 66 666 centimes · pas de 66665.99... · débit = crédit = 3 333 300
LED-07
Annulation — écriture inverse
P1 Bon ISSUED annulé
  1. Annuler le bon
  2. Vérifier l'entrée CANCEL
LedgerEntry CANCEL : VOUCHER_LIABILITY → PROVISION_COMPANY · solde provision restauré · SUM globale équilibrée
LED-08
Expiration — écriture forfeit
P1 Bon ISSUED expiré
  1. Déclencher cron expiration
  2. Vérifier ledger
LedgerEntry EXPIRE : VOUCHER_LIABILITY → EXPIRED_FORFEIT · montant = remainingValue à l'expiration
Parcours E2E complets — Playwright
Ces 4 parcours sont P0 obligatoires. À exécuter sur Android bas de gamme (Tecno Y4 / réseau 3G) ET desktop. Playwright configuré pour les deux viewports.
IDParcoursPrioDescription complèteRésultat attenduStatut
E2E-01
Émission → QR → Scan → Reversement
P0
  1. COMPANY_ADMIN émet un bon de 20 000 FCFA pour +221771234567
  2. Wave confirme via webhook → bon ISSUED
  3. Bénéficiaire reçoit SMS · ouvre /app/wallet · voit le bon
  4. Bénéficiaire ouvre le QR plein écran
  5. Commerçant scanne le QR sur /pos/scan
  6. Commerçant saisit 15 000 FCFA · confirme
  7. Cron T+1 déclenche reversement commerçant
Chaque étape : statut correct en DB · ledger équilibré · SMS reçu · bon PARTIAL · MERCHANT_PAYABLE = 14 700 FCFA · reversement effectué
E2E-02
Import CSV 500 lignes → SMS → Scan 50 bons
P0
  1. RH uploade CSV 500 lignes (provision suffisante)
  2. 500 bons créés en DB
  3. 500 SMS envoyés via queue Bull
  4. Simuler 50 scans POS simultanés sur 50 bons différents
500 bons créés · 500 SMS en queue · 50 validations réussies · aucun double-dépense · ledger équilibré
E2E-03
Test de charge k6 — 100 VUs sur 1 bon
P0
  1. Créer 1 bon ISSUED
  2. Lancer k6 : 100 VUs simultanées, chacune tente de valider le même bon
Exactement 1 validation réussie · 99 rejets (409) · bon → USED · SELECT FOR UPDATE empêche toute race condition
E2E-04
Mode dégradé offline — QR accessible sans réseau
P0
  1. Bénéficiaire ouvre le wallet en ligne · QR chargé une première fois
  2. Passer en mode avion
  3. Rouvrir /app/wallet/[voucherId]
  4. Commerçant scanne (il a du réseau)
QR affiché depuis cache SW · scannable par le POS · validation côté commerçant réussie malgré l'absence de réseau côté bénéficiaire
Commandes d'exécution
# Tests unitaires (Vitest)
npm run test:unit
# Tests E2E Playwright
npm run test:e2e
# Tests de charge k6
npm run test:load
# Typecheck strict
npm run typecheck
# Lint
npm run lint
Jeux de données de test — Comptes & Identifiants
Flux de premier login (tous les comptes) :
1. Saisir le téléphone ou l'email → 2. Recevoir OTP 111111 → 3. Créer son propre mot de passe (min 8 caractères)

Identifiant : téléphone E.164 ou email — au choix, pour tous les rôles.
Montants en DB : toujours en centimes FCFA — 10 000 FCFA = 1 000 000 centimes.
OTP bypass : activé via variable TEST_PHONES sur Railway — les numéros ci-dessous reçoivent toujours le code 111111.

Administrateurs RH Entreprise — Dashboard kado.sn/dashboard

NomTéléphoneEmailEntrepriseRôleOTP
Aminata Diallo +221 70 000 00 01 abouna.dieye@fluxia.sn TechSN Sénégal · rh@techsn.sn COMPANY_ADMIN 111111
Boubacar Diallo +221 70 000 00 02 contact@fluxia.sn COMPANY_ADMIN 111111

Bénéficiaires — Portefeuille kado.sn/app/login

#NomTéléphoneRôleOTP
Ben. 1Ibrahima Fall +221 70 000 00 10 BENEFICIARY 111111
Ben. 2Fatou Ndiaye +221 70 000 00 11 BENEFICIARY 111111
Ben. 3Moussa Ba +221 70 000 00 12 BENEFICIARY 111111

Commerçants — POS kado.sn/pos/login

EnseigneResponsableTéléphoneEmailCatégorieOTP
Le TerangaSeydou Seck+221 77 600 10 01contact@leteranga.snFOOD111111
Pharmacie Santé PlusNdéye Diop+221 77 600 20 02contact@santeplus.snHEALTH111111
KadoShop En LigneOumar Gaye+221 77 600 30 03contact@kadoshop.snRETAIL111111
Auchan VDNMoussa Sarr+221 77 500 00 01auchan@kado-test.snRETAIL111111
Brioche DoréeFatou Kine+221 77 500 00 02brioche@kado-test.snFOOD111111
Total NgorOmar Fall+221 77 500 00 03total@kado-test.snGENERAL111111
Decathlon DakarAmadou Diop+221 77 500 00 04decathlon@kado-test.snRETAIL111111
Casino SahmSophie Ndiaye+221 77 500 00 05casino@kado-test.snRETAIL111111

Éditeur — Ajouter / Modifier un compte de test

Après toute modification, copier la commande générée et l'exécuter via railway run dans le terminal.
# Remplir les champs ci-dessus

Ajouter un compte dans le seed permanent (prisma/seed-production.ts)

Pour que le compte survive à une réinitialisation du seed, l'ajouter dans prisma/seed-production.ts → tableau users ou merchants, puis :
railway run npx tsx prisma/seed-production.ts
Ops & Commandes Railway
Prérequis : être authentifié sur Railway CLI → railway login --browserless · Saisir le code affiché sur railway.com/activate.

1. Authentification Railway CLI

ÉtapeCommande / ActionNotes
1 npm i -g @railway/cli Mettre à jour le CLI si nécessaire
2 railway login --browserless Génère un code de jumelage (ex. pink-tender-truth)
3 Ouvrir railway.com/activate dans le navigateur Saisir le code affiché dans le terminal
4 Cliquer Authorize sur la page Railway Le terminal affiche "Logged in" automatiquement

2. Débloquer les comptes test (Redis)

Un compte se bloque après 5 tentatives de mot de passe incorrectes (15 min) ou 3 OTP échoués (30 min). Ces blocages sont dans Redis, pas en base.
ÉtapeCommandeRésultat attendu
1 railway run npx tsx prisma/unblock-test-accounts.ts Supprime toutes les clés Redis de blocage pour les 13 numéros test · affiche "aucun blocage actif" ou "X clé(s) supprimée(s)"
# Clés Redis supprimées par numéro :
login_blocked:{phone}
login_attempts:{phone}
otp_blocked:{phone}
otp_tries:{phone}
otp_hourly:{phone}

3. Réinitialiser / recréer les comptes test (DB)

Le seed est upsert-only — il ne supprime aucune donnée existante. Il efface les mots de passe des comptes test pour forcer le flux "créer son mot de passe".
ÉtapeCommandeRésultat attendu
1 railway run npx tsx prisma/seed-production.ts Crée ou met à jour les 5 utilisateurs + 8 commerçants · affiche "✅" pour chaque compte · "passwordHash réinitialisé (N comptes)"
railway run npx tsx prisma/seed-production.ts

4. Procédure complète — remise à zéro des accès test

#CommandeQuand
1 railway login --browserless Si "Unauthorized" dans le terminal
2 railway run npx tsx prisma/unblock-test-accounts.ts Toujours en premier — vide les blocages Redis
3 railway run npx tsx prisma/seed-production.ts Crée les comptes manquants + efface les mots de passe
4 Se connecter avec OTP 111111 → créer son mot de passe Après les étapes 2 et 3

5. Variable d'environnement TEST_PHONES (Railway)

La variable TEST_PHONES sur Railway active le bypass OTP (code fixe 111111) pour les numéros listés — même en production.
VariableValeur
TEST_PHONES +221700000001,+221700000002,+221700000010,+221700000011,+221700000012,+221776001001,+221776002002,+221776003003,+221775000001,+221775000002,+221775000003,+221775000004,+221775000005
⚠ Sécurité : supprimer cette variable avant le lancement commercial — elle permet de contourner l'OTP pour les numéros listés.

6. Autres commandes Railway utiles

UsageCommande
Voir les logs API en temps réelrailway logs --tail
Ouvrir Prisma Studio (interface DB)railway run npx prisma studio
Appliquer une migration en attenterailway run npx prisma migrate deploy
Vérifier le statut des migrationsrailway run npx prisma migrate status
Exécuter une requête SQL directerailway run npx prisma db execute --stdin <<'SQL'
SELECT phone, role, "isActive" FROM "User" ORDER BY "createdAt" DESC LIMIT 20;
SQL
Redémarrer le service APIRailway Dashboard → Service → Restart