API Historique des Achats (ClientBuyHistory)
Module de gestion de l'historique des achats des clients pour le système de fidélisation.
Description
Ce module permet d'enregistrer et de gérer l'historique des achats effectués par les clients. Chaque achat est enregistré ligne par ligne (un article = une ligne dans la base de données), permettant une analyse détaillée des comportements d'achat.
Architecture
Traitement Asynchrone avec RabbitMQ
⚠️ Important: L'enregistrement des achats fonctionne de manière asynchrone via RabbitMQ.
Les applications externes publient directement dans RabbitMQ (pas d'endpoint POST HTTP).
Flux de traitement:
Application externe (Caisse, E-commerce, etc.)
|
| Publie un message via AMQP
v
Exchange RabbitMQ: "fidelite.historique-achat.exchange"
|
| Route vers
v
Queue RabbitMQ: "fidelite.historique-achat"
|
| Consumer écoute
v
Consumer NestJS (ClientBuyHistoryConsumer)
|
| Valide et traite
v
Base de données PostgreSQL
Configuration RabbitMQ:
- Exchange:
fidelite.historique-achat.exchange(type:direct) - Queue:
fidelite.historique-achat - Routing Key:
fidelite.historique-achat - Connexion:
amqp://guest:guest@localhost:5672(développement)
Avantages:
- ✅ Découplage complet entre applications
- ✅ Meilleure scalabilité
- ✅ Résilience aux pics de charge
- ✅ Pas de dépendance HTTP
- ✅ RabbitMQ gère automatiquement la persistance des messages
Structure des fichiers
backend/src/client-buy-history/
├── dto/
│ ├── purchase-item.dto.ts # DTO pour un article du panier
│ ├── create-client-buy-history.dto.ts # DTO pour créer un achat
│ └── update-client-buy-history.dto.ts # DTO pour modifier un achat
├── client-buy-history.service.ts # Logique métier
├── client-buy-history.consumer.ts # Consumer RabbitMQ
├── client-buy-history-public.controller.ts # Endpoints publics
├── client-buy-history-private.controller.ts # Endpoints privés
└── client-buy-history.module.ts # Module NestJS
Schéma de la table
Table : client_buy_history
| Champ | Type | Description |
|---|---|---|
| id | bigint | Identifiant unique |
| clientId | bigint | Référence au client (FK) |
| articleId | text | Identifiant de l'article |
| articleName | text | Nom de l'article |
| quantity | bigint | Quantité achetée |
| category | text | Catégorie (optionnelle) |
| unitPrice | bigint | Prix unitaire (en centimes) |
| totalAmount | bigint | Montant total de l'article (quantity × unitPrice) |
| loyaltyPointsEarned | bigint | Points de fidélité gagnés (0 pour l'instant) |
| purchaseDate | timestamp | Date et heure de l'achat |
| createdAt | timestamp | Date de création de l'enregistrement |
| updatedAt | timestamp | Date de dernière modification |
| deletedAt | timestamp | Date de suppression (soft delete) |
Endpoints
Publication dans RabbitMQ (Applications externes)
⚠️ Important: Il n'y a pas d'endpoint POST HTTP. Les applications doivent publier directement dans RabbitMQ.
Comment publier un message
Les applications externes doivent publier un message dans l'exchange RabbitMQ avec le format suivant :
Configuration de publication:
- Exchange:
fidelite.historique-achat.exchange - Routing Key:
fidelite.historique-achat - Content-Type:
application/json - Delivery Mode:
persistent(2)
Format du message:
{
"action": "create",
"timestamp": "2025-10-23T15:37:39.512Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"clientId": "CLI_0001",
"purchaseDate": "2025-10-23T14:30:00Z",
"purchases": [
{
"articleId": "ART001",
"articleName": "Pomme",
"unitPrice": 250,
"quantity": 3,
"category": "Fruits"
},
{
"articleId": "ART002",
"articleName": "Pain",
"unitPrice": 150,
"quantity": 1,
"category": "Boulangerie"
}
]
}
}
Champs obligatoires:
action: Type d'action (toujours "create" pour l'instant)timestamp: Date/heure de l'événement (format ISO 8601)requestId: UUID unique pour tracer la requêtedata.clientId: Identifiant du clientdata.purchaseDate: Date/heure de l'achatdata.purchases: Tableau d'articles achetés (minimum 1)
Note: Le totalAmount de chaque article est calculé automatiquement par le consumer (quantity × unitPrice).
category est optionnel. S'il est absent, la catégorie sera NULL en base.
Traitement du message:
- Le message est routé vers la queue
fidelite.historique-achat - Le consumer NestJS récupère le message
- Il valide les données et vérifie que le client existe
- Il insère les achats en base de données
- Il ACK le message (succès) ou NACK (erreur)
En cas d'erreur:
- Le message est rejeté (NACK)
- Il est jeté (pas de DLQ configurée actuellement)
- L'erreur est loggée avec le
requestIddans les logs du serveur
Endpoints publics
Base URL : /api/publique/client-buy-history
GET /:clientId - Récupérer les achats d'un client
Récupère l'historique complet des achats d'un client spécifique avec pagination et filtres.
Paramètres URL:
clientId(required) : ID du client
Query parameters:
page(optional, default: 1) : Numéro de pagelimit(optional, default: 10) : Nombre d'éléments par pagedateFrom(optional) : Date de début (format ISO 8601)dateTo(optional) : Date de fin (format ISO 8601)minAmount(optional) : Montant minimum en centimesmaxAmount(optional) : Montant maximum en centimesarticleId(optional) : Filtrer par identifiant d'articlearticleName(optional) : Filtrer par nom d'article (recherche partielle)
Exemple:
GET /api/publique/client-buy-history/123?page=1&limit=10&articleName=Pomme
Réponse (200):
{
"data": [
{
"id": 1,
"clientId": 123,
"articleId": "ART001",
"articleName": "Pomme",
"quantity": 3,
"unitPrice": 250,
"totalAmount": 750,
"loyaltyPointsEarned": 0,
"purchaseDate": "2025-10-23T14:30:00.000Z",
"createdAt": "2025-10-23T15:37:39.512Z",
"updatedAt": "2025-10-23T15:37:39.512Z",
"deletedAt": null
}
],
"meta": {
"total": 1,
"page": 1,
"limit": 10,
"totalPages": 1
}
}
Habitudes d'achats (12 mois)
Base URL : /api/publique/clients/:id/habits
Retourne les catégories et articles les plus achetés par le client sur les 12 derniers mois.
Query params optionnels:
limitItems(défaut: 5)limitCategories(défaut: 5)
Réponse (200):
{
"topCategories": [
{ "category": "Fruits", "totalQuantity": 42, "totalAmount": 10500 },
{ "category": "Boulangerie", "totalQuantity": 21, "totalAmount": 6300 }
],
"topItems": [
{
"articleId": "ART001",
"articleName": "Pomme",
"totalQuantity": 30,
"totalAmount": 7500
},
{
"articleId": "ART010",
"articleName": "Baguette",
"totalQuantity": 18,
"totalAmount": 5400
}
],
"meta": {
"period": "last_12_months",
"asOf": "2025-11-11T10:00:00.000Z",
"limits": { "limitItems": 5, "limitCategories": 5 }
}
}
Endpoints privés (Admin)
Base URL : /api/privee/client-buy-history
POST / - Créer un historique d'achat (sans RabbitMQ)
Crée directement un historique d'achat pour un client dans la base de données, sans passer par RabbitMQ.
⚠️ Note: Cet endpoint est réservé aux administrateurs pour créer des achats manuellement. Les applications externes doivent utiliser RabbitMQ (voir section "Publication dans RabbitMQ").
Body (obligatoire):
{
"clientId": "CLI_0001",
"purchaseDate": "2025-10-24T10:30:00Z",
"purchases": [
{
"articleId": "ART001",
"articleName": "Pomme",
"unitPrice": 250,
"quantity": 3
},
{
"articleId": "ART002",
"articleName": "Banane",
"unitPrice": 180,
"quantity": 2
}
]
}
Champs obligatoires:
clientId: Identifiant métier du client (ex: "CLI_0001")purchaseDate: Date/heure de l'achat au format ISO 8601purchases: Tableau d'articles achetés (minimum 1 article)articleId: Identifiant de l'articlearticleName: Nom de l'articleunitPrice: Prix unitaire en centimesquantity: Quantité achetée (> 0)
Exemple de requête:
curl -X POST http://localhost:3000/api/privee/client-buy-history \
-H "Content-Type: application/json" \
-d '{
"clientId": "CLI_0001",
"purchaseDate": "2025-10-24T10:30:00Z",
"purchases": [
{
"articleId": "ART001",
"articleName": "Pomme",
"unitPrice": 250,
"quantity": 3
}
]
}'
Réponse (201):
{
"message": "2 article(s) ajouté(s) avec succès",
"data": [
{
"id": 1,
"clientId": 123,
"articleId": "ART001",
"articleName": "Pomme",
"quantity": 3,
"unitPrice": 250,
"totalAmount": 750,
"loyaltyPointsEarned": 0,
"purchaseDate": "2025-10-24T10:30:00.000Z",
"createdAt": "2025-10-24T16:54:20.512Z",
"updatedAt": "2025-10-24T16:54:20.512Z",
"deletedAt": null
},
{
"id": 2,
"clientId": 123,
"articleId": "ART002",
"articleName": "Banane",
"quantity": 2,
"unitPrice": 180,
"totalAmount": 360,
"loyaltyPointsEarned": 0,
"purchaseDate": "2025-10-24T10:30:00.000Z",
"createdAt": "2025-10-24T16:54:20.512Z",
"updatedAt": "2025-10-24T16:54:20.512Z",
"deletedAt": null
}
]
}
Erreurs:
- 400 : Données invalides (champs manquants ou format incorrect)
- 404 : Client non trouvé
Différence avec RabbitMQ:
| Critère | POST /api/privee | RabbitMQ |
|---|---|---|
| Traitement | Synchrone | Asynchrone |
| Réponse | Immédiate | Différée |
| Utilisation | Interface admin | Intégrations |
| Résilience | Dépend du serveur | Très haute |
| Découplage | Non | Oui |
GET / - Récupérer tous les achats
Récupère l'historique complet des achats de tous les clients avec pagination et filtres.
Query parameters: (mêmes que l'endpoint public GET /:clientId)
Exemple:
GET /api/privee/client-buy-history?page=1&limit=10&minAmount=500
Réponse (200): (même format que l'endpoint public)
GET /:id - Récupérer un achat spécifique
Récupère les détails d'un achat spécifique par son ID.
Paramètres URL:
id(required) : ID de l'achat
Exemple:
GET /api/privee/client-buy-history/1
Réponse (200):
{
"id": 1,
"clientId": 123,
"articleId": "ART001",
"articleName": "Pomme",
"quantity": 3,
"unitPrice": 250,
"totalAmount": 750,
"loyaltyPointsEarned": 0,
"purchaseDate": "2025-10-23T14:30:00.000Z",
"createdAt": "2025-10-23T15:37:39.512Z",
"updatedAt": "2025-10-23T15:37:39.512Z",
"deletedAt": null
}
Erreurs:
- 404 : L'achat n'existe pas
PATCH /:id - Modifier un achat
Met à jour les informations d'un achat spécifique (article, quantité, prix).
Paramètres URL:
id(required) : ID de l'achat
Body (tous les champs sont optionnels):
{
"articleId": "ART001",
"articleName": "Pomme Rouge",
"unitPrice": 300,
"quantity": 5
}
Réponse (200):
{
"id": 1,
"clientId": 123,
"articleId": "ART001",
"articleName": "Pomme Rouge",
"quantity": 5,
"unitPrice": 300,
"totalAmount": 1500,
"loyaltyPointsEarned": 0,
"purchaseDate": "2025-10-23T14:30:00.000Z",
"createdAt": "2025-10-23T15:37:39.512Z",
"updatedAt": "2025-10-23T15:37:58.498Z",
"deletedAt": null
}
Erreurs:
- 404 : L'achat n'existe pas
- 400 : Données invalides
DELETE /:id - Supprimer un achat (soft delete)
Supprime logiquement un achat (le marque comme supprimé sans le retirer de la base).
Paramètres URL:
id(required) : ID de l'achat
Exemple:
DELETE /api/privee/client-buy-history/1
Réponse (200):
{
"message": "L'achat 1 a été supprimé avec succès"
}
Erreurs:
- 404 : L'achat n'existe pas
Validation des données
PurchaseItemDto
| Champ | Type | Contraintes |
|---|---|---|
| articleId | string | Requis, non vide |
| articleName | string | Requis, non vide |
| unitPrice | number | Requis, >= 0 |
| quantity | number | Requis, > 0 |
| category | string | Optionnel (ex: "Fruits") |
CreateClientBuyHistoryDto
| Champ | Type | Contraintes |
|---|---|---|
| clientId | number | Requis, > 0 |
| purchaseDate | string (ISO 8601) | Requis, format ISO 8601 |
| purchases | PurchaseItemDto[] | Requis, tableau avec au moins 1 élément |
Règles métier
-
Validation du client : Avant de créer un achat, le système vérifie que le
clientIdexiste dans la tableclients. -
Points de fidélité : Pour l'instant,
loyaltyPointsEarnedest toujours mis à 0. Une règle métier pourra être ajoutée ultérieurement. -
Soft delete : Les achats supprimés ne sont pas physiquement retirés de la base mais marqués avec un
deletedAt. Ils n'apparaissent plus dans les résultats de requête. -
Calcul automatique : Le
totalAmountde chaque ligne est calculé automatiquement commequantity × unitPrice. Chaque ligne représente un article distinct. -
Tri par défaut : Les résultats sont toujours triés par
purchaseDateen ordre décroissant (du plus récent au plus ancien).
Exemples d'utilisation
Cas 1 : Enregistrer un achat en caisse
curl -X POST http://localhost:3000/api/publique/client-buy-history \
-H "Content-Type: application/json" \
-d '{
"clientId": 123,
"purchaseDate": "2025-10-23T14:30:00Z",
"purchases": [
{"articleId": "ART001", "articleName": "Pomme", "unitPrice": 250, "quantity": 3},
{"articleId": "ART002", "articleName": "Pain", "unitPrice": 150, "quantity": 1},
{"articleId": "ART003", "articleName": "Lait", "unitPrice": 120, "quantity": 5}
]
}'
Cas 2 : Consulter l'historique d'un client
curl -X GET "http://localhost:3000/api/publique/client-buy-history/123?page=1&limit=20"
Cas 3 : Rechercher tous les achats de pommes
curl -X GET "http://localhost:3000/api/privee/client-buy-history?articleName=Pomme"
Cas 4 : Corriger une erreur de saisie
curl -X PATCH http://localhost:3000/api/privee/client-buy-history/1 \
-H "Content-Type: application/json" \
-d '{"quantity": 5, "unitPrice": 300}'
Cas 5 : Annuler un achat erroné
curl -X DELETE http://localhost:3000/api/privee/client-buy-history/1
Surveillance et logs
Le service utilise le système de logging NestJS avec le préfixe [FIDELITE][ClientBuyHistoryService] et [FIDELITE][ClientBuyHistory*Controller].
Exemples de logs :
Création de l'historique d'achat pour le client 1232 article(s) ajouté(s) à l'historiqueRécupération des achats du client 123 avec options: {...}Achat 1 mis à jour avec succèsAchat 1 supprimé avec succès
Documentation Swagger
La documentation Swagger complète est disponible à l'URL : http://localhost:649/api/swagger
Les endpoints sont regroupés sous deux tags :
Historique des achats (Public)Historique des achats (Privé)
Notes techniques
-
Pagination : Implémentée avec les paramètres
pageetlimit. Les métadonnées incluenttotal,page,limit, ettotalPages. -
Filtres : Tous les filtres sont cumulatifs (AND). Le filtre
articleNameutilise ILIKE pour une recherche insensible à la casse. -
Soft delete : Utilise le champ
deletedAtde la table. Tous les SELECT incluent automatiquementWHERE deletedAt IS NULL. -
Type safety : Utilise les types inférés de Drizzle ORM (
typeof clientBuyHistoryTable.$inferSelect). -
DrizzleService : Utilise la méthode
getDb()pour accéder à l'instance de base de données.
Évolutions futures possibles
- Calcul automatique des points de fidélité selon le niveau du client et le montant
- Export des achats au format CSV/Excel
- Statistiques d'achat (top articles, fréquence d'achat, etc.)
- Recommandations basées sur l'historique
- Notifications pour les achats suspects ou inhabituels
- Intégration avec un système de caisse via webhook
Support
Pour toute question ou problème, consulter :
- La documentation Swagger :
http://localhost:649/api/swagger - Les logs du backend
- Le fichier CLAUDE.md à la racine du projet