En développement

CodeSale — Marketplace de biens digitaux

Une marketplace prête pour la production pour acheter et vendre des codes digitaux, construite dès le départ avec la rigueur d’un système financier.

Full StackProjet soloNode.jsNext.jsMongoDBStripeStripe Connect

CodeSale est une marketplace pair-à-pair où les vendeurs listent des biens digitaux (clés de jeux, cartes cadeaux, licences logicielles) et où les acheteurs les achètent avec une livraison garantie. Chaque transaction est protégée par escrow : les fonds sont retenus jusqu’à confirmation de livraison par l’acheteur, avec un système complet de résolution des disputes en cas de problème. La plateforme est conçue pour gérer de l’argent réel, de vrais vecteurs de fraude et de vrais cas limites.

Les marketplaces de biens digitaux existantes partagent souvent les mêmes failles structurelles :

  • Les vendeurs subissent des chargebacks qu’ils ne peuvent pas contester, parce que le paiement est direct et non protégé par escrow.
  • Les acheteurs se font piéger avec des codes invalides, parce que la livraison n’est pas vérifiée.
  • Les plateformes absorbent une fraude qu’elles ne peuvent pas détecter, parce qu’il n’y a ni risk scoring ni suivi comportemental.
  • Les disputes sont résolues subjectivement, parce qu’il n’existe pas de modèle structuré de preuves.

CodeSale est conçu pour contenir économiquement ces quatre problèmes par l’architecture, pas par la politique.

Chaque surface financière du système possède une couche d’autorité unique. Aucun mouvement d’argent n’est implicite.

Modèle de paiement

Le checkout direct par carte est désactivé de façon permanente. Tous les achats passent par un wallet, et le wallet est alimenté uniquement par des top-ups Stripe. Les chargebacks ne peuvent toucher qu’un top-up wallet, jamais une commande directement. La surface de chargeback est réduite à une couche unique et contenable.

Système d’escrow

Chaque commande crée un verrou escrow via un ledger append-only. Les fonds ne bougent pas : ce sont les entrées ledger qui bougent. Release et refund sont des opérations symétriques. Le ledger est la source unique de vérité ; escrowStatus sur la commande est un miroir mis en cache, pas l’autorité.

Cycle de vie des disputes

Les disputes suivent une machine à états stricte : OPEN → UNDER_REVIEW → NEEDS_RESPONSE → WON / LOST → CLOSED. Chaque transition est validée contre la matrice d’états, encapsulée dans une transaction MongoDB et ajoutée à une collection immuable DisputeEvent protégée par concurrence optimiste.

Moteur de risk scoring

Chaque dispute génère un snapshot de preuves immuable au moment de sa création. Un moteur de scoring déterministe produit des niveaux de risque acheteur et vendeur : NORMAL → WARNING → AT_RISK → RESTRICTED → BANNED. Ces niveaux pilotent automatiquement les restrictions — visibilité des listings, éligibilité payout, limites de dispute — sans intervention manuelle de l’admin.

Architecture des payouts

Les payouts vendeurs passent par un système multi-couches : PayoutEligibilityService filtre tous les payouts, WithdrawalDomainService gère les demandes initiées par les vendeurs, WithdrawalExecutionService fait le pont vers les payouts par commande en FIFO, et PayoutService (commit en 3 phases) est la seule entité qui appelle Stripe. Payouts automatisés et retraits vendeurs convergent vers un seul chemin d’exécution.

Atomicité de l’inventaire

La livraison du code est atomique. La transaction checkout exécute dans une seule session MongoDB : verrouiller le code avec findOneAndUpdate, revalider le flag de risque du wallet, débiter le wallet, marquer la commande comme terminée. Si une étape échoue, la transaction rollback. La double vente sous concurrence est structurellement impossible.

01

Checkout wallet-only avant toute autre fonctionnalité financière

Désactiver tôt le checkout direct par carte a structuré tout le modèle de chargeback. Au lieu de construire une détection de fraude pour des paiements Stripe par commande, toute la surface de chargeback a été contenue dans les top-ups wallet. Tous les systèmes financiers en aval sont devenus plus simples et plus auditables.

02

Le ledger comme seule source de vérité

Deux systèmes financiers parallèles existaient au départ : un modèle legacy basé sur les statuts et un modèle basé sur les types d’entrées ledger. Supprimer formellement le legacy a verrouillé l’invariant : les soldes vendeurs dérivent strictement de types d’entrées ledger immuables. Pas de champs de statut, pas de calculs dérivés côté frontend. Cela élimine toute une classe de bugs de dérive financière.

03

Cycle de dispute séparé des résultats financiers

Les transitions d’état des disputes sont totalement découplées des mouvements d’argent. Refund et release sont des appels de service séparés qui modifient l’état de dispute en effet secondaire. Support gère donc le cycle de vie, Admin exécute l’action financière, et aucun des deux ne peut déclencher accidentellement l’autre.

04

Payout découplé de la résolution des disputes

Une version précédente appelait PayoutService directement après la libération de l’escrow. Après refactor, la résolution de dispute écrit uniquement des entrées ledger et place eligibilityStatus à ELIGIBLE. Le worker payout récupère ensuite les commandes éligibles indépendamment. Cela élimine les états bloqués où la dispute est résolue mais le payout échoue.

05

Développement piloté par roadmap avec invariants explicites

Chaque étape de la roadmap verrouille des invariants que les étapes suivantes ne peuvent pas violer : les commandes sont ledger-only. Stripe ≠ escrow. Wallet ≠ carte. Les décisions architecturales prises à l’étape 26 restent appliquées à l’étape 75, non pas par mémoire, mais par contrainte documentée.

Concurrence dans les transactions financières

Deux acheteurs tentent d’acheter simultanément le dernier code disponible. Résolu avec des transactions MongoDB utilisant findOneAndUpdate avec un filtre strict : une seule session peut verrouiller atomiquement un code. Combiné à la concurrence optimiste sur les transitions de dispute filtrées par statut courant, le système empêche à la fois la double vente et la double finalisation.

Isolation des chargebacks

Un acheteur alimente son wallet, achète un code, puis ouvre un chargeback Stripe sur le top-up. Sans isolation, cela crée une dette vendeur. Solution : les chargebacks marquent le wallet acheteur en WALLET_AT_RISK et bloquent les transactions futures, mais ne touchent jamais les gains vendeur. Les gains vendeur dérivent des entrées ledger, pas de l’état du wallet : ils sont structurellement isolés.

Intégrité des preuves de dispute

Utiliser AuditLog pour l’historique des disputes avait un défaut critique : un TTL de 90 jours. Les longues disputes perdaient leur historique. Une collection séparée DisputeEvent a été introduite — append-only, sans TTL, indexée par disputeId et createdAt — écrite atomiquement dans la même transaction que chaque transition de statut.

Race condition dans le refresh token

La migration frontend vers des cookies httpOnly a introduit une race condition classique : deux réponses 401 simultanées déclenchent chacune un refresh, la seconde échoue parce que le token a déjà été rotaté, et l’utilisateur est déconnecté. Résolu avec une promesse de refresh partagée : tous les 401 concurrents attendent la même promesse, et un seul appel refresh atteint le backend.

BackendNode.js — Fastify, CommonJS
Base de donnéesMongoDB avec transactions multi-documents
Frontend — BuyerNext.js
Frontend — Seller / Admin / SupportReact
PaiementsStripe Connect, Webhooks, Wallet Top-ups
Stockage imagesImageKit
Stockage documentsCloudflare R2
AuthJWT avec cookies httpOnly, protection CSRF
76étapes de développement planifiées et documentées avant implémentation
4rôles utilisateur — Buyer, Seller, Support, Admin — chacun avec des surfaces d’autorité distinctes
1invariant financier que chaque étape doit respecter : le ledger est la seule source de vérité
0paiements directs par carte — tout le modèle de paiement passe par la couche wallet

CodeSale est actuellement en développement actif. Le moteur financier central — wallet, escrow, ledger, disputes et payouts — est implémenté et durci. Le travail restant couvre le moteur de risk scoring, l’UX de retrait vendeur, l’enforcement des tests automatisés et le durcissement production avant lancement.

Étude de cas CodeSale | Ragheb Barhoumi