Skip to Content
Leads Entreprise (Grands Comptes)

Leads Entreprise (Grands Comptes)

Ce document présente le module “Grands Comptes” (apps/api/src/leads + apps/store/app/grands-comptes + apps/dashboard/app/(dashboard)/leads-entreprise) : un formulaire de contact B2B public, traité manuellement par un administrateur via une file d’attente en temps réel.


1. Vue d’ensemble

“Grands Comptes” cible les gros acheteurs B2B (restauration collective, distributeurs, transformateurs) qui ne passent pas par le parcours d’achat classique du Store, mais souhaitent être contactés pour négocier des tarifs au volume, des commandes récurrentes ou des délais de paiement adaptés.

Le mécanisme est volontairement simple : un formulaire public sans authentification (POST /leads/enterprise) crée un EnterpriseLead, et l’administrateur le traite dans une file dédiée du dashboard (PENDING → CONTACTED → CLOSED).

[!IMPORTANT] Ce dépôt n’a aucune capacité d’envoi d’email ou de SMS (pas de SMTP, pas de passerelle SMS configurée). Il n’existe donc aucune notification automatique envoyée à l’entreprise qui soumet sa demande, ni de confirmation par email. Le seul mécanisme de notification est une file d’attente admin, poussée en temps réel via SSE — voir §4. Le suivi du contact réel (appel téléphonique, email manuel) est entièrement hors du système, effectué par l’équipe commerciale.


2. Modèle de Données

EnterpriseLead

ChampTypeDescription
idString (UUID)Identifiant unique.
companyNameStringRaison sociale de l’entreprise demandeuse.
contactNameStringNom de la personne à contacter.
emailStringEmail professionnel du contact.
phoneStringTéléphone du contact.
estimatedVolumeString?Volume estimé (texte libre, ex: "500kg / semaine"), optionnel.
messageString?Besoin décrit librement par le demandeur, optionnel.
statusLeadStatusPENDING (défaut) → CONTACTED → CLOSED.
createdAt / updatedAtDateTimeHorodatage.

[!NOTE] EnterpriseLead n’a aucune relation Prisma vers User : la demande est soumise sans compte, avant toute authentification. Ce n’est pas un modèle “denormalisé pour l’historique” comme AuditLog — c’est structurellement un formulaire de contact anonyme, indépendant du reste du schéma.

LeadStatus (enum)

PENDING → demande reçue, pas encore traitée CONTACTED → un commercial a pris contact CLOSED → dossier clôturé (converti ou non)

La progression est strictement linéaire dans l’UI admin : il n’existe pas de bouton pour revenir en arrière (CLOSED → CONTACTED, par exemple).


3. Rôles et Autorisations (RBAC)

EndpointMéthodeRôle RequisDescription / Règle métier
/leads/enterprisePOSTPublicSoumettre une demande de contact. Même convention que POST /auth/register : aucune authentification requise.
/leads/enterpriseGETADMINLister les demandes, avec filtre optionnel ?status=PENDING|CONTACTED|CLOSED.
/leads/enterprise/:idPATCHADMINFaire avancer le statut d’une demande ({ status }).

[!NOTE] Les deux endpoints admin sont protégés par JwtAuthGuard + RolesGuard + @Roles(UserRole.ADMIN), la même combinaison que pour PATCH /countries/:id ou PATCH /products/:id/sponsorship.


4. Notification Temps Réel (SSE)

Il n’y a pas d’email envoyé à la création d’une demande. À la place, leads.service.ts pousse un événement SSE à tous les administrateurs connectés via SseService (le même service @Global() utilisé par le module de sponsorisation) :

this.sseService.broadcastToAdmins('lead.enterprise.created', { id: lead.id, companyName: lead.companyName, contactName: lead.contactName, });

Côté dashboard, DashboardEventsListener.tsx écoute cet événement (parmi d’autres : sponsorship.requested, vendor.registration.created…) sur un flux EventSource('/api/events') monté une seule fois dans le layout, pour tous les rôles :

source.addEventListener('lead.enterprise.created', (event) => { const data: EnterpriseLeadCreatedPayload = JSON.parse((event as MessageEvent).data); toast.warning(`Nouvelle demande Grands Comptes : "${data.companyName}" (${data.contactName})`); router.refresh(); });

[!NOTE] Le filtrage par rôle est fait côté serveur dans SseService.broadcastToAdmins (qui n’envoie qu’aux clients dont role === 'ADMIN') : un vendeur ou un acheteur connecté au même flux /api/events ne recevra jamais cet événement, même si le listener est monté globalement dans le layout.

AuditService est bien utilisé dans ce module, mais uniquement lors du changement de statut (updateStatus), pas à la création : chaque transition de statut est journalisée avec l’ancien et le nouveau statut en métadonnées ({ from: existing.status, to: updated.status }), et seulement si le statut a réellement changé.


5. Logique Métier Clés et Flux

A. Soumission publique (POST /leads/enterprise)

Aucune vérification de doublon, aucun rate-limiting applicatif visible dans le service : chaque soumission crée une nouvelle ligne EnterpriseLead en PENDING, puis déclenche la notification SSE. Les champs obligatoires (companyName, contactName, email, phone) sont validés côté formulaire Store (required HTML) et côté server action (actions.ts) avant l’appel API — le DTO API lui-même (CreateLeadDto) ne porte pas de décorateurs class-validator visibles dans le fichier, donc la validation stricte des formats (email, téléphone) repose sur la couche formulaire, pas sur l’API.

B. Traitement admin (PATCH /leads/enterprise/:id)

  1. Le service vérifie que la demande existe (404 Not Found sinon).
  2. Il met à jour status.
  3. Si le statut a changé, il journalise l’action via AuditService (action "Traitement demande Grands Comptes", sévérité INFO).

Il n’y a aucune validation de transition au niveau service (par exemple, rien n’empêche techniquement de repasser de CLOSED à PENDING via un appel API direct) — c’est l’interface du dashboard qui restreint les transitions à un sens unique.

C. Compteur badge sidebar

getPendingLeadsCount() (dans actions.ts, apps/dashboard/app/(dashboard)/leads-entreprise/) appelle GET /leads/enterprise?status=PENDING et renvoie la longueur du tableau. Ce compte est chargé dans apps/dashboard/app/(dashboard)/layout.tsx et transmis en prop à Sidebar (pendingLeadsCount), qui affiche un badge numérique sur l’entrée de menu “Grands Comptes” — même mécanique que le badge des demandes de sponsorisation en attente ou des vendeurs en attente de validation.


6. Interface Publique (Store)

La page apps/store/app/grands-comptes/page.tsx présente une page marketing statique (arguments : tarifs négociés, commandes récurrentes, interlocuteur dédié) à côté du formulaire GrandsComptesForm.tsx :

  • Formulaire client ('use client') piloté par useActionState, sans authentification.
  • Champs : companyName*, contactName*, email*, phone*, estimatedVolume (optionnel), message (optionnel).
  • En cas de succès (state.success), le formulaire est remplacé par un message de confirmation (“Un membre de notre équipe commerciale vous recontactera très prochainement.”) — pas de redirection, pas d’email de confirmation envoyé au demandeur (voir la note en §1).

7. Interface d’Administration (Dashboard)

La page apps/dashboard/app/(dashboard)/leads-entreprise/page.tsx (réservée au rôle ADMIN) organise les demandes en trois sections empilées, chacune avec son propre compteur dans le titre :

  1. Demandes en attente (PENDING)
  2. En cours de traitement (CONTACTED)
  3. Clôturées (CLOSED)

Chaque ligne de tableau affiche entreprise, contact (nom + email + téléphone), volume estimé, message et date de réception, avec un bouton d’action fourni par LeadStatusButtons.tsx (même dossier) :

  • Statut PENDING → bouton “Marquer contactée” → PATCH { status: 'CONTACTED' }.
  • Statut CONTACTED → bouton “Clôturer” → PATCH { status: 'CLOSED' }.
  • Statut CLOSED → aucun bouton (état terminal dans l’UI).

8. Guide des Endpoints et Exemples de Payloads

Soumission d’une demande (POST /leads/enterprise)

  • Accès : Public, aucun header d’authentification.
  • Requête :
    { "companyName": "Cantine Centrale SARL", "contactName": "Jean Dupont", "email": "contact@cantine-centrale.com", "phone": "+237 6XX XXX XXX", "estimatedVolume": "500kg / semaine", "message": "Recherche un fournisseur régulier de légumes bio pour notre cuisine centrale." }
  • Réponse (201 Created) :
    { "id": "b3e1f9c2-7a4d-4e2b-9f1a-6c8d0e2f4a1b", "companyName": "Cantine Centrale SARL", "contactName": "Jean Dupont", "email": "contact@cantine-centrale.com", "phone": "+237 6XX XXX XXX", "estimatedVolume": "500kg / semaine", "message": "Recherche un fournisseur régulier de légumes bio pour notre cuisine centrale.", "status": "PENDING", "createdAt": "2026-06-23T10:00:00.000Z", "updatedAt": "2026-06-23T10:00:00.000Z" }

Liste filtrée (GET /leads/enterprise?status=PENDING)

  • Accès : Admin uniquement (Header Authorization: Bearer <token_admin>).
  • Réponse (200 OK) : tableau d’EnterpriseLead, trié par createdAt décroissant.

Traitement d’une demande (PATCH /leads/enterprise/:id)

  • Accès : Admin uniquement.
  • Requête :
    { "status": "CONTACTED" }
  • Réponse (200 OK) :
    { "id": "b3e1f9c2-7a4d-4e2b-9f1a-6c8d0e2f4a1b", "companyName": "Cantine Centrale SARL", "contactName": "Jean Dupont", "email": "contact@cantine-centrale.com", "phone": "+237 6XX XXX XXX", "estimatedVolume": "500kg / semaine", "message": "Recherche un fournisseur régulier de légumes bio pour notre cuisine centrale.", "status": "CONTACTED", "createdAt": "2026-06-23T10:00:00.000Z", "updatedAt": "2026-06-23T10:15:00.000Z" }
  • Erreur (404 Not Found) :
    { "statusCode": 404, "message": "Demande Grands Comptes avec l'ID 'b3e1f9c2-...' introuvable." }

9. Bonnes Pratiques de Développement

  1. Ne pas supposer qu’une notification email part réellement : toute évolution de ce module doit soit brancher une vraie passerelle SMTP/SMS (aucune n’existe aujourd’hui dans ce repo), soit continuer à s’appuyer uniquement sur SSE + traitement manuel côté admin.
  2. Ne pas fier la confidentialité de l’endpoint public à autre chose qu’à l’absence de données sensibles retournées : POST /leads/enterprise est volontairement ouvert sans authentification (comme POST /auth/register), donc ne jamais y faire transiter ou renvoyer d’information nécessitant un contrôle d’accès.
  3. Respecter le flux à sens unique de statut côté UI, même si l’API ne l’impose pas techniquement — toute nouvelle surface d’administration (ex: un futur export CSV, une API mobile) doit reproduire cette même contrainte métier plutôt que de permettre un retour arrière arbitraire.
Last updated on