Aller au contenu principal

Validations des données clients

Cette documentation décrit en détail toutes les règles de validation appliquées aux données des clients du programme de fidélité.

Vue d'ensemble

Les validations sont implémentées à l'aide de class-validator dans les DTOs (Data Transfer Objects). Elles s'appliquent automatiquement lors de la création et de la mise à jour des clients.

Localisation du code

  • DTO de création: backend/src/clients/dto/create-client.dto.ts
  • DTO de mise à jour: backend/src/clients/dto/update-client.dto.ts
  • Validateur personnalisé: backend/src/clients/validators/is-date-not-in-future.validator.ts

Champs obligatoires

nom (string)

Règles:

  • ✅ Obligatoire
  • ✅ Doit être une chaîne de caractères

Messages d'erreur:

  • "Le nom est obligatoire"
  • "Le nom doit être une chaîne de caractères"

Exemples:

Valide

{ "nom": "Dupont" }
{ "nom": "Martin-Dubois" }

Invalide

{ "nom": "" }          // Vide
{ "nom": null } // Null
{ "nom": 123 } // Pas une chaîne
// nom manquant

prenom (string)

Règles:

  • ✅ Obligatoire
  • ✅ Doit être une chaîne de caractères

Messages d'erreur:

  • "Le prénom est obligatoire"
  • "Le prénom doit être une chaîne de caractères"

Exemples:

Valide

{ "prenom": "Jean" }
{ "prenom": "Marie-Claire" }

Invalide

{ "prenom": "" }       // Vide
{ "prenom": null } // Null
// prenom manquant

adresseMail (string)

Règles:

  • ✅ Obligatoire
  • ✅ Format email valide
  • ✅ Unique dans la base de données

Messages d'erreur:

  • "L'adresse mail est obligatoire"
  • "L'adresse mail n'est pas valide"
  • "Un client avec cette adresse mail existe déjà" (409 Conflict)

Exemples:

Valide

{ "adresseMail": "jean.dupont@example.com" }
{ "adresseMail": "user+test@domain.fr" }

Invalide

{ "adresseMail": "invalid-email" }          // Format invalide
{ "adresseMail": "user@" } // Domaine manquant
{ "adresseMail": "existing@example.com" } // Déjà existant (409)
// adresseMail manquant

Champs optionnels

civilite (enum)

Règles:

  • ⚪ Optionnel
  • ✅ Si fourni, doit être: "M", "Mme" ou "Mx"

Message d'erreur:

  • "La civilité doit être une des valeurs suivantes: M, Mme, Mx (reçu: {value})"

Exemples:

Valide

{ "civilite": "M" }
{ "civilite": "Mme" }
{ "civilite": "Mx" }
// civilite omis

Invalide

{ "civilite": "Mr" }        // Valeur non autorisée
{ "civilite": "Madame" } // Doit être "Mme"
{ "civilite": "F" } // Valeur non autorisée

anniversaire (date)

Règles:

  • ⚪ Optionnel
  • ✅ Format ISO 8601: YYYY-MM-DD
  • ✅ Ne doit pas être dans le futur

Messages d'erreur:

  • "La date d'anniversaire doit être au format YYYY-MM-DD (ex: 1990-05-15)"
  • "La date d'anniversaire ne peut pas être dans le futur"

Exemples:

Valide

{ "anniversaire": "1990-05-15" }
{ "anniversaire": "2000-12-31" }
// anniversaire omis

Invalide

{ "anniversaire": "15/05/1990" }     // Format incorrect
{ "anniversaire": "1990-5-15" } // Mois/jour sans zéro
{ "anniversaire": "2030-01-01" } // Dans le futur
{ "anniversaire": "invalid-date" } // Pas une date

Note: La validation anti-futur compare la date à minuit (00:00:00) du jour actuel.


telephone (string)

Règles:

  • ⚪ Optionnel
  • ✅ Si fourni, ne peut contenir que:
    • Chiffres: 0-9
    • Espaces
    • Caractères: +, -, (, ), .

Message d'erreur:

  • "Le téléphone ne peut contenir que des chiffres, espaces et caractères +()-."

Exemples:

Valide

{ "telephone": "0612345678" }
{ "telephone": "+33 6 12 34 56 78" }
{ "telephone": "01-23-45-67-89" }
{ "telephone": "(+33) 612345678" }
{ "telephone": "+33.6.12.34.56.78" }
// telephone omis

Invalide

{ "telephone": "abc123" }           // Lettres non autorisées
{ "telephone": "06#12345678" } // Caractère # non autorisé
{ "telephone": "string" } // Texte non numérique

adresse (string)

Règles:

  • ⚪ Optionnel
  • ✅ Doit être une chaîne de caractères

Message d'erreur:

  • "L'adresse doit être une chaîne de caractères"

Exemples:

Valide

{ "adresse": "123 Rue de la Paix, 75001 Paris" }
{ "adresse": "45 Avenue des Champs-Élysées" }
{ "adresse": "Appartement 5B, 10 Boulevard Saint-Michel" }
// adresse omis

Invalide

{ "adresse": 123 }           // Doit être une chaîne
{ "adresse": null } // Null non autorisé (omettre le champ)

idClient (string)

Règles:

  • ⚪ Optionnel
  • ✅ Doit être une chaîne de caractères

Message d'erreur:

  • "L'identifiant client doit être une chaîne de caractères"

Exemples:

Valide

{ "idClient": "CLI123456" }
{ "idClient": "ABC-XYZ-001" }
// idClient omis

niveauFidelisation (enum)

Règles:

  • ⚪ Optionnel
  • ✅ Si fourni, doit être: "Standard", "Premium" ou "Platine"
  • 🔄 Défaut: "Standard" si non fourni

Message d'erreur:

  • "Le niveau de fidélisation doit être: Standard, Premium ou Platine (reçu: {value})"

Exemples:

Valide

{ "niveauFidelisation": "Standard" }
{ "niveauFidelisation": "Premium" }
{ "niveauFidelisation": "Platine" }
// niveauFidelisation omis → défaut "Standard"

Invalide

{ "niveauFidelisation": "Gold" }       // Valeur non autorisée
{ "niveauFidelisation": "standard" } // Sensible à la casse
{ "niveauFidelisation": "VIP" } // Valeur non autorisée

Comportement par défaut: Si niveauFidelisation n'est pas fourni lors de la création, il est automatiquement défini à "Standard".


pointsFidelite (number)

Règles:

  • ⚪ Optionnel
  • ✅ Doit être un nombre entier
  • ✅ Doit être >= 0 (pas de valeurs négatives)

Messages d'erreur:

  • "Les points de fidélité doivent être un nombre entier"
  • "Les points de fidélité ne peuvent pas être négatifs"

Exemples:

Valide

{ "pointsFidelite": 0 }
{ "pointsFidelite": 100 }
{ "pointsFidelite": 9999 }
// pointsFidelite omis

Invalide

{ "pointsFidelite": -100 }      // Négatif
{ "pointsFidelite": 12.5 } // Décimal
{ "pointsFidelite": "100" } // Chaîne au lieu de nombre

Protection importante: Cette validation empêche toute manipulation malveillante ou erreur qui créerait un solde de points négatif.


dateDebutFidelisation (date)

Règles:

  • ⚪ Optionnel
  • ✅ Format ISO 8601: YYYY-MM-DD
  • 🔄 Auto-rempli: Si niveauFidelisation est fourni mais pas cette date, elle est définie à la date du jour

Message d'erreur:

  • "La date de début de fidélisation doit être au format YYYY-MM-DD (ex: 2023-10-10)"

Exemples:

Valide

{ "dateDebutFidelisation": "2023-10-10" }
{ "dateDebutFidelisation": "2024-01-15" }
// dateDebutFidelisation omis

Invalide

{ "dateDebutFidelisation": "10/10/2023" }   // Format incorrect
{ "dateDebutFidelisation": "string" } // Pas une date
{ "dateDebutFidelisation": "2023-10-1" } // Jour sans zéro

Comportement automatique:

// Si niveauFidelisation est fourni mais pas dateDebutFidelisation
{
"niveauFidelisation": "Premium"
// → dateDebutFidelisation sera automatiquement la date du jour
}

Validation personnalisée

IsDateNotInFuture

Fichier: backend/src/clients/validators/is-date-not-in-future.validator.ts

Cette validation personnalisée vérifie qu'une date n'est pas dans le futur. Elle est utilisée pour le champ anniversaire.

Implémentation:

@ValidatorConstraint({ async: false })
export class IsDateNotInFutureConstraint
implements ValidatorConstraintInterface
{
validate(value: any): boolean {
if (!value) {
return true; // Autorise null/undefined (géré par @IsOptional)
}

const inputDate = new Date(value);
const today = new Date();
today.setHours(0, 0, 0, 0); // Reset l'heure à minuit

return inputDate <= today;
}

defaultMessage(): string {
return "La date d'anniversaire ne peut pas être dans le futur";
}
}

Utilisation:

@IsOptional()
@IsDateString()
@IsDateNotInFuture()
anniversaire?: string;

Gestion des erreurs

Format de réponse d'erreur

Toutes les erreurs de validation retournent un statut 400 Bad Request avec le format suivant:

{
"message": ["Message d'erreur 1", "Message d'erreur 2"],
"error": "Bad Request",
"statusCode": 400
}

Types d'erreurs HTTP

Code HTTPTypeCas d'usageExemple
400Bad RequestErreurs de validation des champsEmail invalide, nom manquant, etc.
409ConflictEmail déjà existantTentative de créer un client avec un email existant
404Not FoundClient non trouvéTentative d'accéder à un client inexistant

Erreurs multiples

Si plusieurs validations échouent, tous les messages d'erreur sont retournés ensemble:

Requête:

{
"civilite": "Invalid",
"nom": "",
"prenom": "Test",
"adresseMail": "not-an-email",
"pointsFidelite": -50
}

Réponse (400):

{
"message": [
"La civilité doit être une des valeurs suivantes: M, Mme, Mx (reçu: Invalid)",
"Le nom est obligatoire",
"L'adresse mail n'est pas valide",
"Les points de fidélité ne peuvent pas être négatifs"
],
"error": "Bad Request",
"statusCode": 400
}

Gestion des erreurs côté frontend

Le frontend (ClientsPage.tsx) implémente une gestion complète des erreurs :

Erreur 409 (Email existant)

if (response.status === 409) {
setValidationErrors({
adresseMail: errorData.message || 'Cette adresse email est déjà utilisée',
});
return;
}

L'erreur est affichée directement sous le champ email.

Erreurs 400 (Validation)

Les erreurs de validation du backend sont automatiquement mappées aux champs correspondants :

if (response.status === 400) {
const newErrors = {};

// Parse les erreurs NestJS (tableau de messages)
if (Array.isArray(errorData.message)) {
errorData.message.forEach((msg: string) => {
const msgLower = msg.toLowerCase();

// Mapping automatique vers les champs
if (msgLower.includes('civilité')) {
newErrors.civilite = msg;
} else if (msgLower.includes('nom')) {
newErrors.nom = msg;
}
// ... autres champs
});
}

setValidationErrors(newErrors);
}

Champs supportés pour l'affichage d'erreurs :

  • civilite - Civilité invalide
  • nom - Nom manquant ou invalide
  • prenom - Prénom manquant ou invalide
  • adresseMail - Email manquant, invalide ou existant
  • telephone - Téléphone avec caractères invalides
  • anniversaire - Date invalide ou dans le futur
  • niveauFidelisation - Niveau de fidélisation invalide
  • general - Erreur non mappée à un champ spécifique

Validation frontend

En plus des validations backend, le frontend effectue des validations préventives :

Email :

function validateEmail(email: string): string | null {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) return "L'adresse email est requise";
if (!emailRegex.test(email)) return "Format d'email invalide";
return null;
}

Téléphone :

function validatePhone(phone: string | null): string | null {
if (!phone) return null; // Optionnel
const phoneRegex = /^0[1-9](\s?\d{2}){4}$/;
if (!phoneRegex.test(phone)) {
return 'Format de téléphone invalide (ex: 06 12 34 56 78)';
}
return null;
}

Date d'anniversaire :

function validateBirthday(birthday: string | null): string | null {
if (!birthday) return null; // Optionnel
const inputDate = new Date(birthday);
const today = new Date();
today.setHours(0, 0, 0, 0);

if (inputDate > today) {
return "La date d'anniversaire ne peut pas être dans le futur";
}
return null;
}

Ces validations s'exécutent avant l'envoi de la requête au backend, offrant un retour immédiat à l'utilisateur.


Tests de validation

Tester les validations

Pour tester les validations manuellement:

# Test: Points négatifs (devrait échouer)
curl -X POST 'http://localhost:3000/api/publique/clients' \
-H 'Content-Type: application/json' \
-d '{
"nom": "Test",
"prenom": "User",
"adresseMail": "test@example.com",
"pointsFidelite": -100
}'

# Test: Date invalide (devrait échouer)
curl -X POST 'http://localhost:3000/api/publique/clients' \
-H 'Content-Type: application/json' \
-d '{
"nom": "Test",
"prenom": "User",
"adresseMail": "test2@example.com",
"dateDebutFidelisation": "invalid-date"
}'

# Test: Données valides (devrait réussir)
curl -X POST 'http://localhost:3000/api/publique/clients' \
-H 'Content-Type: application/json' \
-d '{
"civilite": "M",
"nom": "Dupont",
"prenom": "Jean",
"anniversaire": "1990-05-15",
"adresseMail": "jean.dupont@example.com",
"telephone": "0612345678",
"pointsFidelite": 100
}'

Récapitulatif des règles

ChampObligatoireTypeContraintesDéfaut
nomStringNon vide-
prenomStringNon vide-
adresseMailStringEmail valide, unique-
civiliteEnumM, Mme, Mx-
anniversaireDateYYYY-MM-DD, pas futur-
telephoneStringChiffres et +()-.-
idClientString--
niveauFidelisationEnumStandard, Premium, Platine"Standard"
pointsFideliteNumberEntier >= 0-
dateDebutFidelisationDateYYYY-MM-DDDate du jour si niveau fourni

Légende:

  • ✅ = Obligatoire
  • ⚪ = Optionnel