Skip to Content
Gestion des Produits et Catégories

Gestion des Produits et Catégories

Ce document présente l’architecture, la logique métier et la spécification des API du module de gestion des produits et des catégories (apps/api/src/products). Ce module permet aux administrateurs de configurer la taxonomie du catalogue et aux vendeurs de publier et mettre à jour leurs offres.


1. Modèle de Données et Relations

Le catalogue repose sur ces entités principales définies dans le schéma Prisma :

  1. ProductCategory : Représente une catégorie de produit (ex: “Électronique”).
  2. Product : Représente un article proposé par un vendeur. Un produit appartient à une seule catégorie et est lié à un vendeur (User).
  3. ProductImage : Représente une illustration du produit. Un produit peut posséder plusieurs images.
  4. Certification : Labels que peut porter un produit (relation many-to-many), ex: “Bio”.
  5. Review : Avis clients (note + commentaire), alimentent Product.avgRating/reviewCount.
  6. Country (optionnel) : dénormalisé sur Product.countryId depuis le pays du vendeur à la création, pour filtrer le catalogue sans jointure.

[!NOTE] Le champ Product.sponsorshipStatus (et les champs associés sponsorshipPrice/sponsorshipPricingMode/sponsorshipDurationDays/sponsorshipExpiresAt) pilote la mise en avant payante du produit dans le tri du catalogue — voir Sponsorisation des produits pour ce sujet, non détaillé ici.


2. Rôles et Autorisations (RBAC)

La gestion du catalogue applique des contrôles d’accès stricts basés sur les rôles des utilisateurs (UserRole) :

EndpointMéthodeRôle RequisDescription / Règle métier
/categoriesPOSTADMINCréer une nouvelle catégorie.
/categoriesGETPublicRécupérer toutes les catégories.
/categories/:idOrSlugGETPublicObtenir une catégorie par son ID ou son slug.
/categories/:idPATCHADMINModifier le nom ou la description d’une catégorie.
/categories/:idDELETEADMINSupprimer une catégorie (autorisé uniquement si elle est vide).
/productsPOSTSELLER (+ vendorStatus: APPROVED)Publier un nouveau produit (lié au vendeur connecté).
/productsGETPublicLister et filtrer les produits actifs (isActive: true).
/products/:idOrSlugGETPublicObtenir le détail complet d’un produit (avec images, vendeur et catégorie).
/products/:idPATCHSELLER (Propriétaire) | ADMINModifier un produit et mettre à jour ses images.
/products/:idDELETESELLER (Propriétaire) | ADMINSupprimer un produit (suppression en cascade des images).

3. Logiques Métier Clés et Flux

A. Flux de Création d’un Produit (POST /products)

Lorsqu’un vendeur crée un produit, le système effectue les étapes suivantes :

  1. Vérification du statut de validation vendeur (User.vendorStatus) : un vendeur auto-inscrit encore PENDING/REJECTED se voit refuser la création (403 Forbidden, message VENDOR_NOT_APPROVED) — voir Validation des comptes vendeur.
  2. Vérification de l’existence de la catégorie demandée (categoryId).
  3. Génération d’un slug unique et propre au SEO à partir du nom du produit.
  4. Insertion imbriquée du produit et de ses images associées.

B. Algorithme de Résolution des Collisions de Slugs

Les URLs des produits reposent sur des slugs générés à partir de leur nom commercial (ex: iPhone 13 ➔ iphone-13). Afin d’éviter les doublons (qui violeraient la contrainte @unique de la base de données), le service implémente un mécanisme de résolution de collision :

  1. L’application nettoie le texte via une fonction utilitaire slugify (conversion en minuscules, suppression des accents et des caractères spéciaux, remplacement des espaces par des tirets).
  2. Si le slug existe déjà dans la table Product, l’application ajoute un suffixe numérique incrémental (-1, -2, etc.) jusqu’à trouver un slug disponible.

Code de résolution (dans products.service.ts) :

private async generateUniqueSlug(name: string): Promise<string> { const baseSlug = slugify(name); let slug = baseSlug; let count = 1; while (true) { const exists = await this.prisma.product.findUnique({ where: { slug }, }); if (!exists) { return slug; } slug = `${baseSlug}-${count}`; count++; } }

C. Recherche Avancée et Filtrage (GET /products)

L’endpoint de récupération de la liste des produits est ouvert au public et propose un système complet de filtrage et de pagination :

  • Filtres Disponibles :
    • categoryId : Filtrer par catégorie spécifique.
    • sellerId : Voir les produits d’un vendeur particulier.
    • countryId : Filtrer par pays (champ dénormalisé sur Product, voir Gestion des Pays).
    • search : Recherche textuelle floue (insensible à la casse) sur le name ou la description.
    • minPrice / maxPrice : Filtrer les produits dans une fourchette de prix.
    • minRating : Filtrer par note moyenne minimale (Product.avgRating).
    • certifications : Filtrer par certifications portées par le produit.
    • sponsoredOnly / sponsorshipStatus : Filtres liés au module de sponsorisation (ignorés si le module est désactivé — voir Sponsorisation des produits).
    • isActive : true (défaut, public), false, ou all (vendeur/admin uniquement, pour voir aussi les produits désactivés).
  • Pagination & Tri :
    • Les paramètres page (défaut 1) et limit (défaut 10) contrôlent la taille des résultats.
    • Les paramètres sortBy (ex: price, createdAt) et sortOrder (asc ou desc) gèrent le tri.

[!NOTE] Visibilité Publique : Par défaut (isActive non précisé), la recherche publique ne renvoie que les produits pour lesquels isActive est égal à true. Les produits désactivés par le vendeur ou modérés par l’administrateur sont automatiquement masqués.

D. Suppression Sécurisée et Cascades

Pour préserver l’intégrité de la base de données, des règles spécifiques s’appliquent lors des suppressions :

  1. Catégorie (DELETE /categories/:id) : Une catégorie ne peut pas être supprimée si elle contient encore des produits actifs ou inactifs. Le service effectue un décompte des produits liés (_count.products) et lève une BadRequestException en cas de présence de produits.
  2. Produit (DELETE /products/:id) : Pour éviter de violer les clés étrangères, le service procède en deux étapes transactionnelles :
    • Suppression de toutes les entrées dans ProductImage liées au produit.
    • Suppression de la ligne principale du produit dans Product.

4. Guide des Endpoints et Exemples de Payloads

Création d’une Catégorie (POST /categories)

  • Accès : Admin uniquement (Header Authorization: Bearer <token_admin>).
  • Requête :
    { "name": "Ordinateurs Portables", "description": "PC portables, Chromebooks et Ultrabooks" }
  • Réponse (201 Created) :
    { "id": "e932b7a9-fa59-4b68-80f6-2244bb5f8423", "name": "Ordinateurs Portables", "slug": "ordinateurs-portables", "description": "PC portables, Chromebooks et Ultrabooks", "createdAt": "2026-06-23T10:00:00.000Z", "updatedAt": "2026-06-23T10:00:00.000Z" }

Création d’un Produit (POST /products)

  • Accès : Vendeur uniquement (Header Authorization: Bearer <token_vendeur>).
  • Requête :
    { "name": "MacBook Pro 14 M3", "description": "Apple MacBook Pro avec puce M3, 16 Go de RAM, 512 Go SSD.", "price": 1999.99, "stock": 5, "sku": "MAC-M3-14-001", "categoryId": "e932b7a9-fa59-4b68-80f6-2244bb5f8423", "images": [ { "url": "https://example.com/images/macbook-1.jpg", "position": 1 }, { "url": "https://example.com/images/macbook-2.jpg", "position": 2 } ] }
  • Réponse (201 Created) : Le produit est créé et retourne les objets images créés (avec isPrimary calculé à true pour l’image en position 1) :
    { "id": "7bf3b934-8c81-42ab-ba0c-99d9b62fb91e", "name": "MacBook Pro 14 M3", "slug": "macbook-pro-14-m3", "description": "Apple MacBook Pro avec puce M3, 16 Go de RAM, 512 Go SSD.", "price": "1999.99", "stock": 5, "sku": "MAC-M3-14-001", "sellerId": "user-uuid-vendeur-123", "categoryId": "e932b7a9-fa59-4b68-80f6-2244bb5f8423", "isActive": true, "createdAt": "2026-06-23T10:05:00.000Z", "updatedAt": "2026-06-23T10:05:00.000Z", "images": [ { "id": "img-uuid-1", "productId": "7bf3b934-8c81-42ab-ba0c-99d9b62fb91e", "url": "https://example.com/images/macbook-1.jpg", "position": 1, "isPrimary": true, "createdAt": "2026-06-23T10:05:00.000Z", "updatedAt": "2026-06-23T10:05:00.000Z" }, { "id": "img-uuid-2", "productId": "7bf3b934-8c81-42ab-ba0c-99d9b62fb91e", "url": "https://example.com/images/macbook-2.jpg", "position": 2, "isPrimary": false, "createdAt": "2026-06-23T10:05:00.000Z", "updatedAt": "2026-06-23T10:05:00.000Z" } ], "category": { "id": "e932b7a9-fa59-4b68-80f6-2244bb5f8423", "name": "Ordinateurs Portables", "slug": "ordinateurs-portables", "description": "PC portables, Chromebooks et Ultrabooks", "createdAt": "2026-06-23T10:00:00.000Z", "updatedAt": "2026-06-23T10:00:00.000Z" } }

5. Bonnes Pratiques de Développement

  1. Champs Monétaires / Décimaux : Dans l’application Node/TypeScript, Prisma renvoie le champ price sous forme d’un objet Decimal (provenant de la bibliothèque decimal.js). Lors des sérialisations JSON, NestJS le convertit en chaîne de caractères ("1999.99") pour préserver sa précision. Pour toute opération arithmétique côté backend, utilisez toujours des instances Decimal (par exemple : new Decimal(product.price).mul(quantity)) et évitez de repasser par des nombres flottants JavaScript standard (Number()).
  2. Gestion des IDOR (Insecure Direct Object Reference) : Lors de la mise à jour (PATCH) ou de la suppression (DELETE) d’un produit, ne vous fiez pas uniquement au fait que l’utilisateur possède le rôle SELLER. Le service valide impérativement que product.sellerId === currentUserId. Si l’utilisateur connecté est ADMIN, la validation de possession est contournée pour permettre la modération.
Last updated on