Skip to Content
Sponsorisation des produits

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)

ChampTypeDescription
sponsorshipStatusNONE | PENDING | ACTIVEStatut courant.
sponsorshipPriceDecimal?Tarif verrouillé au moment de la demande (insensible à un changement ultérieur des réglages admin).
sponsorshipPricingModeFIXED | PERCENTAGE | nullMode choisi par le vendeur parmi les options actives au moment de la demande.
sponsorshipDurationDaysInt?Durée verrouillée au moment de la demande.
sponsorshipExpiresAtDateTime?Calculée à l’approbation (now() + sponsorshipDurationDays).

SystemSettings (ligne singleton, réglages admin)

ChampTypeDescription
sponsorshipEnabledBooleanInterrupteur global du module.
sponsorshipFixedEnabledBooleanLe volet “prix fixe” est-il proposé aux vendeurs ?
sponsorshipFixedPriceDecimalPrix fixe (FCFA), utilisé si fixedEnabled = true.
sponsorshipFixedDurationDaysIntDurée (jours) propre au volet prix fixe.
sponsorshipPercentageEnabledBooleanLe volet “pourcentage” est-il proposé aux vendeurs ?
sponsorshipPercentageFloatPourcentage du prix du produit (ex: 5 = 5%), utilisé si percentageEnabled = true.
sponsorshipPercentageDurationDaysIntDuré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 : BadRequestException si fixedEnabled et percentageEnabled seraient tous deux false).


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

  1. 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 être NONE, mode doit correspondre à une option active) → PENDING, prix/mode/durée verrouillés.
  2. Admin : PATCH /products/:id/sponsorship { status: 'ACTIVE' | 'NONE' } → approuve (calcule sponsorshipExpiresAt) 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 sponsoredOnly sont ignorés dans GET /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.orderId est aujourd’hui obligatoire et unique (1:1 avec Order) : 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èle Payment existant.
  • 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)

EndpointMéthodeRôle requisDescription
/settings/sponsorshipGETPublicLire l’état du module + la tarification active.
/settings/sponsorshipPATCHADMINModifier l’état du module et/ou la tarification (mise à jour partielle).
/products/:id/sponsorship/quoteGETSELLER (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/requestPOSTSELLER (propriétaire)Demander la sponsorisation avec le mode choisi ({ mode }) (NONE → PENDING), prix/mode/durée verrouillés.
/products/:id/sponsorshipPATCHADMINApprouver (ACTIVE) ou refuser/désactiver (NONE).
/events/sseGET (SSE)AuthentifiéFlux d’événements temps réel (sponsorship.requested, sponsorship.decided).
Last updated on