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 :
ProductCategory: Représente une catégorie de produit (ex: “Électronique”).Product: Représente un article proposé par un vendeur. Un produit appartient à une seule catégorie et est lié à un vendeur (User).ProductImage: Représente une illustration du produit. Un produit peut posséder plusieurs images.Certification: Labels que peut porter un produit (relation many-to-many), ex: “Bio”.Review: Avis clients (note + commentaire), alimententProduct.avgRating/reviewCount.Country(optionnel) : dénormalisé surProduct.countryIddepuis le pays du vendeur à la création, pour filtrer le catalogue sans jointure.
[!NOTE] Le champ
Product.sponsorshipStatus(et les champs associéssponsorshipPrice/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) :
| Endpoint | Méthode | Rôle Requis | Description / Règle métier |
|---|---|---|---|
/categories | POST | ADMIN | Créer une nouvelle catégorie. |
/categories | GET | Public | Récupérer toutes les catégories. |
/categories/:idOrSlug | GET | Public | Obtenir une catégorie par son ID ou son slug. |
/categories/:id | PATCH | ADMIN | Modifier le nom ou la description d’une catégorie. |
/categories/:id | DELETE | ADMIN | Supprimer une catégorie (autorisé uniquement si elle est vide). |
/products | POST | SELLER (+ vendorStatus: APPROVED) | Publier un nouveau produit (lié au vendeur connecté). |
/products | GET | Public | Lister et filtrer les produits actifs (isActive: true). |
/products/:idOrSlug | GET | Public | Obtenir le détail complet d’un produit (avec images, vendeur et catégorie). |
/products/:id | PATCH | SELLER (Propriétaire) | ADMIN | Modifier un produit et mettre à jour ses images. |
/products/:id | DELETE | SELLER (Propriétaire) | ADMIN | Supprimer 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 :
- Vérification du statut de validation vendeur (
User.vendorStatus) : un vendeur auto-inscrit encorePENDING/REJECTEDse voit refuser la création (403 Forbidden, messageVENDOR_NOT_APPROVED) — voir Validation des comptes vendeur. - Vérification de l’existence de la catégorie demandée (
categoryId). - Génération d’un slug unique et propre au SEO à partir du nom du produit.
- 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 :
- 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). - 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é surProduct, voir Gestion des Pays).search: Recherche textuelle floue (insensible à la casse) sur lenameou ladescription.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, ouall(vendeur/admin uniquement, pour voir aussi les produits désactivés).
- Pagination & Tri :
- Les paramètres
page(défaut1) etlimit(défaut10) contrôlent la taille des résultats. - Les paramètres
sortBy(ex:price,createdAt) etsortOrder(ascoudesc) gèrent le tri.
- Les paramètres
[!NOTE] Visibilité Publique : Par défaut (
isActivenon précisé), la recherche publique ne renvoie que les produits pour lesquelsisActiveest é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 :
- 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 uneBadRequestExceptionen cas de présence de produits. - 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
ProductImageliées au produit. - Suppression de la ligne principale du produit dans
Product.
- Suppression de toutes les entrées dans
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 (avecisPrimarycalculé àtruepour l’image en position1) :{ "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
- Champs Monétaires / Décimaux :
Dans l’application Node/TypeScript, Prisma renvoie le champ
pricesous forme d’un objetDecimal(provenant de la bibliothèquedecimal.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 instancesDecimal(par exemple :new Decimal(product.price).mul(quantity)) et évitez de repasser par des nombres flottants JavaScript standard (Number()). - 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ôleSELLER. Le service valide impérativement queproduct.sellerId === currentUserId. Si l’utilisateur connecté estADMIN, la validation de possession est contournée pour permettre la modération.