Sponsorisation des produits
Ce document présente le module de sponsorisation (apps/api/src/products + apps/api/src/settings + apps/api/src/events) : ce qui est implémenté aujourd’hui, et les évolutions envisagées mais non construites pour l’instant.
1. Vue d’ensemble
La sponsorisation permet à un vendeur de payer pour mettre en avant un produit sur le Store : badge “Sponsorisé”, priorité dans le tri du catalogue, et éligibilité au filtre “Produits sponsorisés”. C’est l’un des trois axes de classement des produits, avec les certifications et les avis clients (reviews).
Le module est un interrupteur global activable/désactivable par un administrateur (SystemSettings.sponsorshipEnabled). Quand il est désactivé, tout affichage lié disparaît (Store et dashboards), sans perte des données déjà en base.
2. Modèle de données
Product (champs sponsorisation)
| Champ | Type | Description |
|---|---|---|
sponsorshipStatus | NONE | PENDING | ACTIVE | Statut courant. |
sponsorshipPrice | Decimal? | Tarif verrouillé au moment de la demande (insensible à un changement ultérieur des réglages admin). |
sponsorshipPricingMode | FIXED | PERCENTAGE | null | Mode choisi par le vendeur parmi les options actives au moment de la demande. |
sponsorshipDurationDays | Int? | Durée verrouillée au moment de la demande. |
sponsorshipExpiresAt | DateTime? | Calculée à l’approbation (now() + sponsorshipDurationDays). |
SystemSettings (ligne singleton, réglages admin)
| Champ | Type | Description |
|---|---|---|
sponsorshipEnabled | Boolean | Interrupteur global du module. |
sponsorshipFixedEnabled | Boolean | Le volet “prix fixe” est-il proposé aux vendeurs ? |
sponsorshipFixedPrice | Decimal | Prix fixe (FCFA), utilisé si fixedEnabled = true. |
sponsorshipFixedDurationDays | Int | Durée (jours) propre au volet prix fixe. |
sponsorshipPercentageEnabled | Boolean | Le volet “pourcentage” est-il proposé aux vendeurs ? |
sponsorshipPercentage | Float | Pourcentage du prix du produit (ex: 5 = 5%), utilisé si percentageEnabled = true. |
sponsorshipPercentageDurationDays | Int | Durée (jours) propre au volet pourcentage. |
Les deux volets sont indépendamment activables, y compris simultanément, et ont chacun leur propre durée (pas de durée partagée). Au moins un volet doit rester actif (validé côté API :
BadRequestExceptionsifixedEnabledetpercentageEnabledseraient tous deuxfalse).
3. Ce qui est implémenté
A. Tarif à deux volets combinables, chacun avec sa propre durée (actuellement actif)
L’admin active indépendamment un ou les deux volets tarifaires, chacun avec son propre prix (ou pourcentage) et sa propre durée — pas de durée partagée entre les deux :
- Prix fixe : un montant en FCFA + une durée en jours, identiques pour tous les produits.
- Pourcentage : un pourcentage du prix du produit lui-même (ex: 5% d’un produit à 10 000 FCFA = 500 FCFA) + sa propre durée en jours, potentiellement différente de celle du prix fixe.
Si un seul volet est actif, la demande du vendeur l’utilise automatiquement (prix et durée de ce volet). Si les deux sont actifs, le devis (GET /products/:id/sponsorship/quote) renvoie les deux options, chacune avec son prix et sa durée propres, et le vendeur choisit celle qu’il préfère au moment de sa demande (POST /products/:id/sponsorship/request { mode: 'FIXED' | 'PERCENTAGE' }). L’admin peut ainsi proposer les deux formules en permanence (par exemple une offre courte à prix fixe et une offre longue au pourcentage), ou n’en garder qu’une active selon la saison/le contexte — au moins une doit toujours rester activée.
Prix, mode choisi et durée (celle du volet choisi) sont calculés et verrouillés au moment de la demande — un changement de réglages après coup n’affecte pas les demandes déjà en cours ou déjà actives.
Aucun paiement réel n’est collecté pour l’instant. Le prix est calculé, affiché au vendeur, et stocké — mais la demande reste soumise à une validation manuelle de l’admin, exactement comme avant l’ajout de la tarification. Voir §4.4 pour l’intégration d’un vrai paiement.
B. Workflow demande → validation admin
- Vendeur :
GET /products/:id/sponsorship/quote→ liste des options actives (une ou deux).POST /products/:id/sponsorship/request { mode }(doit être propriétaire, statut doit êtreNONE, mode doit correspondre à une option active) →PENDING, prix/mode/durée verrouillés. - Admin :
PATCH /products/:id/sponsorship { status: 'ACTIVE' | 'NONE' }→ approuve (calculesponsorshipExpiresAt) ou refuse/désactive (efface prix/mode/durée/expiration).
C. Expiration automatique (paresseuse)
Pas de tâche planifiée (cron) : à chaque lecture publique (GET /products, GET /products/:id), le service repasse en NONE tout produit ACTIVE dont sponsorshipExpiresAt est dépassée, en une seule requête updateMany. Suffisant pour le volume de cette marketplace ; à remplacer par un job planifié (@nestjs/schedule) si la fréquence de consultation devient trop faible pour garantir une expiration réactive.
D. Interrupteur global (module ON/OFF)
GET /settings/sponsorship (public) / PATCH /settings/sponsorship (ADMIN). Quand désactivé :
- Nouvelles demandes et décisions admin bloquées (
403). - Le boost de tri et le filtre
sponsoredOnlysont ignorés dansGET /products. - Store et dashboards masquent tout l’affichage lié (badge, filtre, bouton, page admin — sauf l’interrupteur lui-même pour pouvoir réactiver).
- Les données existantes (produits déjà
ACTIVE/PENDING) sont conservées intactes.
E. Notifications temps réel (SSE)
GET /events/sse (authentifié, filtré par rôle/utilisateur côté serveur). Deux événements :
sponsorship.requested→ poussé à tous les admins connectés à la demande d’un vendeur.sponsorship.decided→ poussé au vendeur concerné à la décision de l’admin.
Le dashboard proxifie ce flux via une route serveur (/api/events) pour ne jamais exposer le JWT au navigateur (le cookie access_token est httpOnly).
4. Roadmap (options envisagées, non implémentées)
Ces options ont été évaluées mais volontairement écartées pour la première version, au profit du tarif à deux volets combinables (A) — plus simple et suffisant pour démarrer. Elles restent des évolutions naturelles.
4.1 Paliers de durée
Au lieu d’une durée unique fixée par l’admin, le vendeur choisirait parmi plusieurs formules à prix dégressif au jour (ex: 7j / 30j / 90j). Nécessiterait une petite table de tarifs (durée → prix) plutôt qu’un couple (prix, durée) unique dans SystemSettings, et un sélecteur côté formulaire de demande.
4.2 Paliers de visibilité (Standard / Premium)
Faire varier le niveau d’exposition plutôt que la durée : un palier “Sponsorisé” (badge normal, boost actuel) et un palier “Sponsorisé Premium” (badge distinct, priorité même parmi les sponsorisés, mise en avant sur la page d’accueil du Store). Impliquerait de faire évoluer SponsorshipStatus (ou d’ajouter un champ tier) et le tri à deux niveaux de boost.
4.3 Forfait de crédits (“jours-produit”)
Le vendeur achèterait un forfait de jours-sponsorisation (ex: 60 jours-produit) à répartir librement entre ses produits, sans repasser par une demande/validation à chaque fois. Nécessiterait un nouveau modèle de solde par vendeur (SponsorshipCredit), une déduction quotidienne, et un historique de consommation — plus complexe que les autres options.
4.4 Paiement réel (intégration Payment/PaymentMethod)
Aujourd’hui, le prix est calculé et affiché mais aucune transaction n’a lieu — la validation admin reste le seul contrôle réel. Pour collecter un vrai paiement :
- Réutiliser
PaymentMethod(déjà utilisé pour les commandes) pour le choix du moyen de paiement. Payment.orderIdest aujourd’hui obligatoire et unique (1:1 avecOrder) : il faudrait le rendre optionnel et ajouter une relation alternative (ex:Payment.sponsorshipProductId?), ou créer un modèle dédié (SponsorshipPayment) pour ne pas complexifier le modèlePaymentexistant.- Décider si l’activation devient automatique dès paiement confirmé (
PAID), ou si la validation admin reste une étape supplémentaire après paiement.
4.5 Abonnement vendeur mensuel
Un abonnement récurrent (ex: 10 000 FCFA/mois) sponsorisant automatiquement jusqu’à N produits actifs choisis par le vendeur. Le plus lourd à opérer correctement : le système Payment actuel est conçu pour des transactions ponctuelles (une commande = un paiement), pas pour des paiements récurrents avec gestion d’échecs et de relances.
5. Endpoints (état actuel)
| Endpoint | Méthode | Rôle requis | Description |
|---|---|---|---|
/settings/sponsorship | GET | Public | Lire l’état du module + la tarification active. |
/settings/sponsorship | PATCH | ADMIN | Modifier l’état du module et/ou la tarification (mise à jour partielle). |
/products/:id/sponsorship/quote | GET | SELLER (propriétaire) | Devis (une option par volet actif, chacune avec son propre prix et sa propre durée) avant de confirmer une demande. |
/products/:id/sponsorship/request | POST | SELLER (propriétaire) | Demander la sponsorisation avec le mode choisi ({ mode }) (NONE → PENDING), prix/mode/durée verrouillés. |
/products/:id/sponsorship | PATCH | ADMIN | Approuver (ACTIVE) ou refuser/désactiver (NONE). |
/events/sse | GET (SSE) | Authentifié | Flux d’événements temps réel (sponsorship.requested, sponsorship.decided). |