Retour aux projets
Commerce numérique & cartes cadeaux

Odigix

Une marketplace de cartes cadeaux numériques en Algérie. Plus de 30 000 utilisateurs inscrits y achètent des crédits PlayStation, des abonnements Netflix et des recharges mobiles — avec des moyens de paiement locaux qui fonctionnent vraiment.

Voir le site
Laravel APIMySQLRedisHorizonNext.jsInertia + ReactLaravel MCPEvolution API (WhatsApp)

Le problème

L'Algérie a une population jeune et à l'aise avec le numérique — l'âge médian tourne autour de 29 ans — mais presque aucune infrastructure pour acheter des produits numériques en ligne. Les cartes cadeaux internationales (PlayStation, Netflix, Steam, Apple) ne sont pas vendues localement. Les quelques revendeurs qui existent passent par les messages privés Instagram et les groupes WhatsApp : paiement manuel, code envoyé par capture d'écran. Ça fonctionne, mais c'est lent, propice aux arnaques et impossible à faire passer à l'échelle.

Le paysage des paiements aggrave la situation. Les cartes bancaires et postales algériennes ne fonctionnent pas sur les plateformes internationales : les passerelles étrangères sont exclues. Toute plateforme qui vend des produits numériques ici doit intégrer des prestataires de paiement locaux — sur Odigix, SlickPay et OneClick — et construire sa propre couche autour.

Odigix devait devenir « l'Amazon des produits numériques » en Algérie : un endroit où l'on cherche, on clique, on paie avec les moyens qu'on utilise déjà, et on reçoit son code instantanément.

Ce que j'ai construit

J'ai rejoint le projet en tant que seul ingénieur backend et j'ai construit la plateforme de zéro : en freelance à partir de 2023, puis à temps plein depuis janvier 2026. Le premier frontend a été construit par un autre développeur avec Quasar (Vue.js) ; c'est moi qui ai conçu le contrat d'API et développé chaque endpoint derrière.

Architecture backend

Client → Nginx → Laravel API → MySQL
                   ↓
              Redis (cache + queue)
                   ↓
            Horizon workers → Supplier APIs
                   ↓
            Payment webhooks ← Provider callbacks

Catalogue produits :

  • Les produits sont organisés par catégorie (gaming, streaming, mobile, logiciels) avec prise en charge des variantes régionales (une carte PSN pour l'Algérie n'est pas la même que pour la France)
  • Les prix sont récupérés depuis les API des fournisseurs, puis majorés d'une marge configurable par catégorie
  • Les fiches produits sont mises en cache dans Redis avec un TTL de 5 minutes — le catalogue change rarement, donc l'approche « cache d'abord » s'imposait
  • Quand un produit est en rupture chez tous les fournisseurs, il est automatiquement masqué du catalogue

Pipeline de traitement des commandes :

C'est là que se concentre la complexité. Une seule commande touche quatre systèmes :

  1. Panier → Checkout : l'API vérifie la disponibilité du stock, calcule le prix final (y compris les éventuelles règles de tarification dynamique) et crée une commande au statut pending
  2. Paiement : la commande est transmise au prestataire de paiement — SlickPay ou OneClick. Chacun a son parcours, ses notifications et ses modes d'échec ; l'API masque ces différences derrière une interface unique PaymentProvider
  3. Exécution : une fois le paiement confirmé (via webhook), la commande passe en processing. Un worker Horizon la prend en charge, appelle l'API du fournisseur pour acheter le code et stocke la clé d'activation chiffrée en base de données
  4. Livraison : le code s'affiche à l'écran et est envoyé par e-mail et par WhatsApp (via une intégration de l'Evolution API). Si la livraison échoue (e-mail rejeté, navigateur fermé), le code est conservé en séquestre et reste récupérable depuis l'historique des commandes

Tout cela tourne de manière asynchrone via Laravel Horizon. L'utilisateur voit un état « en cours de traitement » qui se met à jour à chaque étape. Le délai de livraison dépend du fournisseur tiers : le système est donc conçu pour rester correct quand un fournisseur est lent ou en panne, pas seulement rapide quand tout va bien.

Rapprochement des paiements :

SlickPay et OneClick confirment les paiements différemment, avec des délais différents. Ni le retour du client depuis la page de paiement, ni une seule notification ne suffisent : le statut est confirmé auprès du prestataire, et une même notification reçue deux fois ne doit rien changer.

J'ai construit un registre (ledger) unifié qui normalise tout cela :

// Simplified — one normalizer per provider (SlickPay, OneClick)
class OrderLedger {
    public function recordPayment(string $orderId, string $provider, array $payload): void
    {
        $normalized = $this->normalize($provider, $payload);
        // Already recorded? Then this is a retry — do nothing.
        if (LedgerEntry::where('provider', $provider)->where('reference', $normalized->reference)->exists()) {
            return;
        }

        $order = $this->matchOrder($normalized); // by the reference we sent to the provider

        DB::transaction(function () use ($order, $normalized) {
            $order->update(['status' => 'paid', 'paid_at' => now()]);
            LedgerEntry::create([
                'order_id' => $order->id,
                'amount' => $normalized->amount,
                'currency' => $normalized->currency,
                'provider' => $normalized->provider,
                'reference' => $normalized->reference,
            ]);
        });
    }
}

Deux règles font l'essentiel du travail. Idempotence : chaque paiement est identifié par prestataire et référence, donc une notification renvoyée ou un double clic ne peuvent pas payer une commande deux fois. Pas de devinette : un paiement qui ne correspond pas à exactement une commande est laissé à un humain, jamais attribué à la commande la plus proche — les cartes cadeaux ont des valeurs fixes, donc les montants identiques sont fréquents.

Fiabilité des API fournisseurs :

Un casse-tête permanent. Certains fournisseurs affichent 99,9 % de disponibilité ; d'autres tombent plusieurs fois par jour. Au début, la panne d'un seul fournisseur provoquait un effet domino : l'utilisateur tentait d'acheter un produit, le fournisseur ne répondait pas, et la commande restait bloquée en processing indéfiniment.

J'ai mis en place un circuit breaker :

  • Si un fournisseur échoue 3 fois en 5 minutes, il passe en circuit_open
  • Tant que le circuit est ouvert, les commandes portant sur ses produits sont soit redirigées vers un fournisseur alternatif, soit mises en file d'attente
  • Au bout de 10 minutes, le circuit passe en half_open : une commande test est envoyée. Si elle réussit, le circuit se referme. Sinon, il reste ouvert 10 minutes de plus

Résultat : environ 80 % d'échecs en moins côté utilisateur. Le catalogue reste disponible même quand certains fournisseurs ne le sont pas.

Conception de la base de données

Le schéma est simple, avec un choix notable : les redemption_codes sont stockés dans une table à part, chiffrés en AES-256 au niveau applicatif, et pas seulement au repos. La table codes contient :

  • order_id (FK)
  • code_value (chiffré)
  • supplier_id (FK)
  • delivered_at (nullable — les codes restent en séquestre jusqu'à ce que l'utilisateur les récupère)
  • expires_at (certains codes ont une date d'expiration fixée par le fournisseur)

La clé de chiffrement est chargée depuis les variables d'environnement et renouvelée chaque trimestre. C'est excessif pour la plupart des applications SaaS, mais un code de carte cadeau, c'est pratiquement de l'argent liquide : si la base fuite, chaque code non livré est de l'argent volé.

Les points difficiles

Des achats fournisseurs idempotents. Un job qui expire alors que le fournisseur a déjà débité Odigix ne doit pas racheter le même code en réessayant. Chaque achat porte une clé d'idempotence par article de commande : une nouvelle tentative ne peut pas devenir un second achat.

Limiter le débit vers les API fournisseurs. Certains fournisseurs imposent des limites strictes (10 requêtes/minute). Avec 30 000 utilisateurs et des ventes flash, ces limites sont vite atteintes. J'ai mis en place un rate limiter à seau de jetons (token bucket) par fournisseur : quand le seau est vide, les commandes sont mises en file d'attente au lieu d'échouer. Lors d'une promotion de type Black Friday, la file a accumulé environ 90 secondes de retard — agaçant, mais bien mieux que des erreurs.

Gérer les exécutions partielles. Il arrive qu'une commande contienne 3 articles et que seuls 2 aboutissent, alors que l'utilisateur a payé les 3. J'ai construit un système d'exécution partielle : les articles réussis sont livrés immédiatement, et les articles en échec sont soit relancés automatiquement, soit remboursés. L'interface affiche un statut par article plutôt qu'un statut global de commande.

Le problème du « il est où mon code ? ». Les utilisateurs paniquent quand ils ont payé et ne reçoivent pas leur code dans les 10 secondes. J'ai ajouté une page de suivi en temps réel qui montre chaque étape du pipeline (paiement reçu → traitement → code récupéré → livré). Le simple fait d'afficher la progression a réduit les tickets de support d'environ 40 %.

Résultats

  • Plus de 30 000 utilisateurs inscrits dès la première année
  • Livraison automatisée depuis plusieurs fournisseurs tiers, avec limitation de débit, reprises et disjoncteurs par fournisseur
  • 99,5 % de disponibilité malgré des API fournisseurs instables
  • Aucune perte de données sur les callbacks de paiement (le registre capte tout)
  • Volume de tickets de support en baisse de 40 % après l'ajout du suivi transparent des commandes

Ce qui change aujourd'hui

Odigix est en cours de refonte avec deux objectifs : le référencement et une exploitation plus rapide.

  • La boutique passe à Next.js. La raison principale est le SEO : une boutique rendue côté serveur donne à chaque produit et chaque catégorie une vraie page indexable, ce que l'application monopage ne permettait pas.
  • Laravel Nova remplacé par une administration sur mesure. L'espace d'administration passe de Laravel Nova à une administration sur mesure, plus puissante, construite avec Laravel, Inertia et React.
  • Du MCP dans l'administration. Avec Laravel MCP, les opérations d'administration sont exposées comme des outils MCP : un assistant IA peut travailler avec les mêmes données et les mêmes actions que l'équipe.

Vous avez un projet similaire ?

Vous voulez vendre en ligne en Algérie avec SlickPay, OneClick ou d'autres moyens de paiement locaux ? Découvrez mon offre d'intégration du paiement en ligne en Algérie ou contactez-moi pour en parler.

Vous avez un projet similaire ?

Expliquez-moi où vous en êtes et ce qui vous bloque. Réponse rapide sur WhatsApp, devis gratuit.