| Module | Nb cas | P0 | P1 | P2 |
|---|---|---|---|---|
| Authentification (OTP / JWT) | 10 | 3 | 4 | 3 |
| Bénéficiaire — Wallet & QR | 12 | 4 | 5 | 3 |
| RH Entreprise — Émission & Dashboard | 14 | 3 | 7 | 4 |
| POS Commerçant — Scan & Validation | 12 | 5 | 5 | 2 |
| Paiements Mobile Money & Webhooks | 10 | 4 | 4 | 2 |
| Sécurité & Anti-fraude | 8 | 4 | 3 | 1 |
| Ledger & Comptabilité | 8 | 4 | 3 | 1 |
| Parcours E2E complets | 4 | 4 | 0 | 0 |
| TOTAL | 78 | 31 | 31 | 16 |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| AUTH-01 | Envoi OTP valide Numéro +221 E.164 valide |
P1 | Numéro inexistant en DB |
|
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) |
|
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é |
|
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é |
|
HTTP 401 · message "Code expiré" | |
| AUTH-05 | Limite d'envoi OTP (3/heure) |
P0 | Numéro non bloqué |
|
3 premiers : HTTP 200 · 4ème : HTTP 429 RATE_LIMIT_EXCEEDED | |
| AUTH-06 | Refresh token — rotation stricte |
P1 | Session active |
|
É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 | — |
|
HTTP 401 Unauthorized | |
| AUTH-08 | RBAC — mauvais rôle |
P1 | Token BENEFICIARY valide |
|
HTTP 403 Forbidden | |
| AUTH-09 | Numéro non E.164 Sénégal |
P2 | — |
|
HTTP 400 · message "Numéro invalide — format +221XXXXXXXXX requis" | |
| AUTH-10 | Déconnexion — blacklist token Redis |
P2 | Session active |
|
Étape 1 : HTTP 200 · Étape 2 : HTTP 401 (token en blacklist Redis) |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| BEN-01 | Affichage wallet — liste des bons |
P1 | Bénéficiaire avec 2 bons ISSUED |
|
2 bons affichés · montants en FCFA (centimes/100) · statuts corrects | |
| BEN-02 | QR code plein écran |
P0 | Bon ISSUED actif |
|
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) |
|
QR affiché depuis cache SW · pas d'erreur réseau visible | |
| BEN-04 | Bon expiré — affichage statut |
P1 | Bon avec expiresAt dépassé |
|
Badge EXPIRÉ · pas de lien vers QR · montant barré | |
| BEN-05 | Bon partiellement utilisé |
P1 | Bon en statut PARTIAL |
|
Solde restant = remainingValue/100 FCFA · statut PARTIAL · QR accessible | |
| BEN-06 | Onglet Offres — chargement |
P1 | Session active |
|
Liste des offres affichée · pas d'écran blanc · pas d'erreur 401 silencieuse | |
| BEN-07 | Onglet Fidélité — chargement |
P1 | Session active |
|
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 |
|
Affiché : "50 000 FCFA" (5 000 000 / 100) · jamais "5000000" brut | |
| BEN-09 | PWA — installation home screen |
P2 | Android Chrome |
|
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) |
|
Badge "En attente" · QR non accessible · message explicatif affiché | |
| BEN-11 | Bon scheduledAt futur |
P2 | Bon avec scheduledAt dans 3 jours |
|
Date d'activation future visible · QR non accessible avant cette date | |
| BEN-12 | Performances 3G — chargement wallet |
P1 | Réseau 3G simulé (throttling) |
|
Wallet visible en moins de 4s · skeletons affichés pendant le chargement |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| RH-01 | Émission bon unitaire |
P0 | COMPANY_ADMIN · provision > montant |
|
Voucher PENDING créé · LedgerEntry ISSUE : PROVISION_COMPANY → VOUCHER_LIABILITY · SMS envoyé | |
| RH-02 | Émission — provision insuffisante |
P0 | Provision = 0 |
|
HTTP 409 INSUFFICIENT_PROVISION · aucun bon créé · ledger non touché | |
| RH-03 | Import CSV — 500 collaborateurs |
P0 | CSV valide 500 lignes · provision suffisante |
|
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 |
|
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é |
|
Voucher → CANCELLED · LedgerEntry CANCEL : VOUCHER_LIABILITY → PROVISION_COMPANY · provision restituée | |
| RH-06 | Annulation bon USED — impossible |
P1 | Bon USED |
|
HTTP 409 · message "Bon déjà utilisé — annulation impossible" | |
| RH-07 | Onboarding — invitation collaborateur |
P1 | COMPANY_ADMIN connecté |
|
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 |
|
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 |
|
HTTP 403 · Dashboard lisible · aucune action de mutation disponible | |
| RH-10 | Émission planifiée (scheduledAt) |
P2 | scheduledAt dans 7 jours |
|
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 |
|
HTTP 403 ou résultat vide · aucun bon de B visible par A | |
| RH-12 | Note RH — max 200 caractères |
P2 | — |
|
HTTP 400 · validation "note max 200 caractères" | |
| RH-13 | Cron expiration bons — 00h01 UTC |
P1 | Bons ISSUED avec expiresAt = hier |
|
Bons → EXPIRED · LedgerEntry EXPIRE : VOUCHER_LIABILITY → EXPIRED_FORFEIT | |
| RH-14 | Export liste bons — CSV |
P2 | Entreprise avec 20 bons |
|
Fichier CSV téléchargé · colonnes : id, beneficiaryPhone, amount (FCFA), status, expiresAt |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| POS-01 | Scan QR valide — validation partielle |
P0 | Bon ISSUED · solde 50 000 FCFA · commerçant actif compatible |
|
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 |
|
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 |
|
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) |
|
HTTP 400 QR_INVALID · comparaison timingSafeEqual · aucune modification en DB | |
| POS-05 | Montant > solde disponible |
P0 | Bon PARTIAL · solde 5 000 FCFA |
|
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é |
|
HTTP 409 VOUCHER_EXPIRED · message clair sur l'écran POS | |
| POS-07 | Type bon incompatible commerçant |
P1 | Bon TRANSPORT chez commerçant FOOD |
|
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 |
|
Caméra active en moins de 2s · pas de menu intermédiaire | |
| POS-09 | Commission kado 5% |
P1 | Bon ISSUED · montant 10 000 FCFA |
|
MERCHANT_PAYABLE = 9 800 FCFA · REVENUE_COMMISSION = 200 FCFA · Math.round (pas de float) | |
| POS-10 | Idempotence validation |
P1 | Même reference idempotence |
|
2ème appel : HTTP 409 DUPLICATE_TRANSACTION · aucune nouvelle écriture ledger | |
| POS-11 | Commerçant SUSPENDED |
P2 | Commerçant statut SUSPENDED |
|
HTTP 403 · message "Compte commerçant suspendu" | |
| POS-12 | Timeout transaction Prisma (5s) |
P2 | DB simulée lente (> 5s) |
|
Transaction annulée après 5s · aucune écriture partielle · bon dans état initial |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| PAY-01 | Webhook Wave — signature valide |
P0 | Secret Wave configuré en ENV |
|
HTTP 200 · bon passé PENDING → ISSUED (emeConfirmedAt renseigné) | |
| PAY-02 | Webhook Wave — signature invalide |
P0 | — |
|
HTTP 401 · aucun traitement · événement logué comme tentative invalide | |
| PAY-03 | Webhook Orange Money — signature valide |
P0 | Secret OM configuré |
|
HTTP 200 · bon activé | |
| PAY-04 | Webhook — rawBody disponible |
P0 | — |
|
HTTP 400 "rawBody non disponible" · pas de crash silencieux | |
| PAY-05 | Provision entreprise via Mobile Money (Bictorys) |
P1 | Entreprise active |
|
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 |
|
LedgerEntry SETTLE : MERCHANT_PAYABLE → MERCHANT_SETTLED · référence idempotence unique | |
| PAY-07 | Retry reversement — 3 tentatives exponential backoff |
P1 | API Mobile Money indisponible |
|
3 tentatives · backoff exponentiel (delay 60s) · après 3 échecs : job "failed" · alerte loguée | |
| PAY-08 | Idempotence reversement |
P1 | Reversement déjà effectué |
|
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 |
|
2ème appel : idempotent (HTTP 200) ou HTTP 409 · aucune double activation de bon | |
| PAY-10 | Queue Bull — appels synchrones bannis |
P2 | — |
|
Tous les appels passent par la queue Bull (via Bictorys) · aucun appel synchrone dans les controllers |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| SEC-01 | Rate limiter global — 100 req/min IP |
P0 | — |
|
101ème requête : HTTP 429 RATE_LIMIT_EXCEEDED | |
| SEC-02 | Rate limiter /auth/otp — 30 req/min |
P0 | — |
|
31ème requête : HTTP 429 | |
| SEC-03 | Détection fraude — validation au centime exact |
P0 | Commerçant avec 10 transactions au centime exact |
|
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 |
|
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 | — |
|
4ème validation : blocage automatique · HTTP 409 ou alerte backoffice | |
| SEC-06 | Secrets — aucun hardcode dans le code |
P1 | — |
|
Zéro occurrence de secrets en dur · tous via process.env | |
| SEC-07 | TypeScript strict — zéro any implicite |
P1 | — |
|
0 erreur TS · "strict": true dans tsconfig · aucun any implicite | |
| SEC-08 | CORS — origines non autorisées |
P2 | ALLOWED_ORIGINS configuré |
|
HTTP 403 · header CORS absent ou restreint |
| ID | Cas de test | Prio | Préconditions | Étapes | Résultat attendu | Statut |
|---|---|---|---|---|---|---|
| LED-01 | Invariant débit = crédit — émission |
P0 | Bon ISSUED |
|
SUM(débit) = SUM(crédit) = 5 000 000 centimes | |
| LED-02 | Invariant débit = crédit — validation POS |
P0 | Bon validé en POS |
|
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 |
|
Erreur runtime ou contrainte DB · aucune modification acceptée | |
| LED-04 | Reference idempotence — unicité UNIQUE |
P0 | LedgerEntry avec reference = "abc123" |
|
Contrainte UNIQUE violée · erreur Prisma · aucune entrée dupliquée | |
| LED-05 | Montants en centimes — jamais Float |
P0 | — |
|
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) |
|
Commission = 66 666 centimes · pas de 66665.99... · débit = crédit = 3 333 300 | |
| LED-07 | Annulation — écriture inverse |
P1 | Bon ISSUED annulé |
|
LedgerEntry CANCEL : VOUCHER_LIABILITY → PROVISION_COMPANY · solde provision restauré · SUM globale équilibrée | |
| LED-08 | Expiration — écriture forfeit |
P1 | Bon ISSUED expiré |
|
LedgerEntry EXPIRE : VOUCHER_LIABILITY → EXPIRED_FORFEIT · montant = remainingValue à l'expiration |
| ID | Parcours | Prio | Description complète | Résultat attendu | Statut |
|---|---|---|---|---|---|
| E2E-01 | Émission → QR → Scan → Reversement |
P0 |
|
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 |
|
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 |
|
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 |
|
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 |
TEST_PHONES sur Railway — les numéros ci-dessous reçoivent toujours le code 111111.
| Nom | Téléphone | Entreprise | Rôle | OTP | |
|---|---|---|---|---|---|
| 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 |
| # | Nom | Téléphone | Rôle | OTP |
|---|---|---|---|---|
| Ben. 1 | Ibrahima Fall | +221 70 000 00 10 | BENEFICIARY | 111111 |
| Ben. 2 | Fatou Ndiaye | +221 70 000 00 11 | BENEFICIARY | 111111 |
| Ben. 3 | Moussa Ba | +221 70 000 00 12 | BENEFICIARY | 111111 |
| Enseigne | Responsable | Téléphone | Catégorie | OTP | |
|---|---|---|---|---|---|
| Le Teranga | Seydou Seck | +221 77 600 10 01 | contact@leteranga.sn | FOOD | 111111 |
| Pharmacie Santé Plus | Ndéye Diop | +221 77 600 20 02 | contact@santeplus.sn | HEALTH | 111111 |
| KadoShop En Ligne | Oumar Gaye | +221 77 600 30 03 | contact@kadoshop.sn | RETAIL | 111111 |
| Auchan VDN | Moussa Sarr | +221 77 500 00 01 | auchan@kado-test.sn | RETAIL | 111111 |
| Brioche Dorée | Fatou Kine | +221 77 500 00 02 | brioche@kado-test.sn | FOOD | 111111 |
| Total Ngor | Omar Fall | +221 77 500 00 03 | total@kado-test.sn | GENERAL | 111111 |
| Decathlon Dakar | Amadou Diop | +221 77 500 00 04 | decathlon@kado-test.sn | RETAIL | 111111 |
| Casino Sahm | Sophie Ndiaye | +221 77 500 00 05 | casino@kado-test.sn | RETAIL | 111111 |
# Remplir les champs ci-dessus
prisma/seed-production.ts → tableau users ou merchants, puis :railway run npx tsx prisma/seed-production.ts
railway login --browserless · Saisir le code affiché sur railway.com/activate.
| Étape | Commande / Action | Notes |
|---|---|---|
| 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 |
| Étape | Commande | Ré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)" |
| Étape | Commande | Ré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)" |
| # | Commande | Quand |
|---|---|---|
| 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 |
TEST_PHONES sur Railway active le bypass OTP (code fixe 111111) pour les numéros listés — même en production.
| Variable | Valeur |
|---|---|
TEST_PHONES |
+221700000001,+221700000002,+221700000010,+221700000011,+221700000012,+221776001001,+221776002002,+221776003003,+221775000001,+221775000002,+221775000003,+221775000004,+221775000005 |
| Usage | Commande |
|---|---|
| Voir les logs API en temps réel | railway logs --tail |
| Ouvrir Prisma Studio (interface DB) | railway run npx prisma studio |
| Appliquer une migration en attente | railway run npx prisma migrate deploy |
| Vérifier le statut des migrations | railway run npx prisma migrate status |
| Exécuter une requête SQL directe | railway run npx prisma db execute --stdin <<'SQL' |
| Redémarrer le service API | Railway Dashboard → Service → Restart |