Gestion des Adresses
Ce document présente l’architecture, la logique métier et la spécification des API du module de gestion des adresses (apps/api/src/addresses). Ce module permet aux utilisateurs (acheteurs, vendeurs, administrateurs) de gérer leurs adresses de livraison et de facturation, indispensables pour passer des commandes.
1. Modèle de Données et Relations
Les adresses sont associées à un utilisateur unique et peuvent être référencées dans plusieurs commandes (Order). La structure repose sur l’entité Address définie dans le schéma Prisma :
Description des Champs de l’Entité Address
id(String, UUID) : Identifiant unique de l’adresse.userId(String) : Identifiant de l’utilisateur propriétaire de l’adresse.addressLine1(String) : Ligne d’adresse principale (ex: “123 Rue de Paris”).addressLine2(String, optionnel) : Complément d’adresse (ex: “Appartement 4B”).city(String) : Ville.province(String, optionnel) : Région, département ou État.country(String) : Pays en texte libre — champ historique, conservé tel quel pour compatibilité.countryId(String, optionnel) : Relation versCountry— source de vérité pour tout calcul (TVA, frais de port, filtrage catalogue). Peut rester non résolue (null) pour les adresses créées avant l’introduction du modèleCountry. Voir Gestion des Pays.postalCode(String, optionnel) : Code postal.latitude/longitude(Float, optionnels) : Géocodées automatiquement (best-effort, jamais bloquant) à partir de ville/province/pays. Voir §5 ci-dessous.isDefault(Boolean) : Indique s’il s’agit de l’adresse par défaut de l’utilisateur.createdAt/updatedAt(DateTime) : Métadonnées d’audit.
[!IMPORTANT]
country(texte) vscountryId(relation) : les deux ne doivent jamais diverger. Si uncountryIdest fourni à la création, le nom duCountrycorrespondant fait foi pour le champ texte librecountry(voir addresses.service.ts) — mais l’inverse n’est jamais recalculé automatiquement.
2. Rôles et Autorisations (RBAC)
Tous les endpoints de la gestion des adresses sont protégés par le garde d’authentification JwtAuthGuard. De plus, l’accès à une ressource spécifique est restreint à son propriétaire :
| Endpoint | Méthode | Rôle Requis | Description / Règle métier |
|---|---|---|---|
/addresses | POST | Connecté | Créer une nouvelle adresse pour le compte utilisateur. |
/addresses | GET | Connecté | Lister toutes les adresses associées à l’utilisateur connecté. |
/addresses/:id | GET | Propriétaire | Obtenir le détail d’une adresse spécifique. |
/addresses/:id | PATCH | Propriétaire | Modifier les détails d’une adresse ou la définir par défaut. |
/addresses/:id | DELETE | Propriétaire | Supprimer une adresse (avec transfert éventuel de l’adresse par défaut). |
[!IMPORTANT] Contrôle de Propriété : Le système vérifie systématiquement que le
userIdde l’adresse ciblée correspond auuserIdextrait du JWT (address.userId === currentUserId). Si ce n’est pas le cas, une exceptionForbiddenException(403) est renvoyée.
3. Logiques Métier Clés et Flux
A. Flux de Création d’une Adresse (POST /addresses)
Lorsqu’un utilisateur crée une adresse, le système applique la logique suivante dans une transaction :
- Comptage des adresses existantes de l’utilisateur.
- Si le compteur est égal à
0ou si le payload spécifie"isDefault": true:- L’adresse en cours de création est marquée comme adresse par défaut.
- Les autres adresses de cet utilisateur sont modifiées pour passer
isDefaultàfalse.
- Sinon, l’adresse est créée normalement.
B. Algorithme de Mutation de l’Adresse par Défaut (isDefault)
La gestion de l’état isDefault requiert des transactions Prisma pour garantir qu’un utilisateur possède toujours exactement une adresse par défaut (si sa liste d’adresses n’est pas vide).
1. À la modification (PATCH /addresses/:id)
Si l’utilisateur change isDefault à true :
- Les autres adresses de l’utilisateur passent à
false. Si l’utilisateur tente de passerisDefaultàfalsesur son adresse par défaut actuelle : - Si l’utilisateur possède une autre adresse, le système promeut automatiquement l’une des autres adresses comme étant par défaut.
- Si c’est sa seule adresse, elle reste par défaut (
isDefaultest forcé àtrue).
2. À la suppression (DELETE /addresses/:id)
Si l’adresse supprimée était l’adresse par défaut :
- Le service recherche l’adresse restante la plus récente de l’utilisateur (triée par
createdAt desc) et la définit comme adresse par défaut.
Code d’implémentation clé (dans addresses.service.ts) :
// Extrait de la mise à jour d'une adresse par défaut
let finalIsDefault = dto.isDefault;
if (dto.isDefault === false && address.isDefault) {
const otherAddress = await tx.address.findFirst({
where: { userId, NOT: { id } },
});
if (otherAddress) {
await tx.address.update({
where: { id: otherAddress.id },
data: { isDefault: true },
});
} else {
finalIsDefault = true;
}
}4. Géocodage automatique (best-effort)
À la création ou modification d’une adresse, GeocodingService tente de résoudre latitude/longitude à partir de city + province + country (le nom résolu, pas le texte brut si un countryId est fourni), via Nominatim (OpenStreetMap) — gratuit, sans clé API.
[!WARNING] Best-effort, jamais bloquant : un échec de géocodage (service indisponible, adresse trop imprécise, quota dépassé) ne doit jamais empêcher la création/modification de l’adresse —
latitude/longituderestent simplementnull. Une adresse sans coordonnées n’apparaît pas comme repère sur la carte publique des producteurs (voir Annuaire public des vendeurs & carte producteurs), mais reste parfaitement valide pour la livraison.
[!NOTE] Nominatim impose un maximum d’1 requête/seconde et exige un
User-Agentidentifiable — respecté ici avec un appel unique par création/modification. Le script de rattrapage backfill-geocoding.ts applique le même délai pour géocoder en lot les adresses existantes qui n’ont pas encore de coordonnées.
5. Guide des Endpoints et Exemples de Payloads
Création d’une Adresse (POST /addresses)
- Requête :
{ "addressLine1": "456 Avenue de la République", "addressLine2": "Bâtiment C, Appt 12", "city": "Lyon", "province": "Auvergne-Rhône-Alpes", "country": "France", "countryId": "cty-fr-uuid", "postalCode": "69002", "isDefault": true } - Réponse (
201 Created) :{ "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "userId": "usr-999-default-uuid", "addressLine1": "456 Avenue de la République", "addressLine2": "Bâtiment C, Appt 12", "city": "Lyon", "province": "Auvergne-Rhône-Alpes", "country": "France", "countryId": "cty-fr-uuid", "postalCode": "69002", "latitude": 45.764043, "longitude": 4.835659, "isDefault": true, "createdAt": "2026-06-24T22:00:00.000Z", "updatedAt": "2026-06-24T22:00:00.000Z" }
Liste des Adresses (GET /addresses)
- Réponse (
200 OK) :[ { "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "userId": "usr-999-default-uuid", "addressLine1": "456 Avenue de la République", "addressLine2": "Bâtiment C, Appt 12", "city": "Lyon", "province": "Auvergne-Rhône-Alpes", "country": "France", "postalCode": "69002", "isDefault": true, "createdAt": "2026-06-24T22:00:00.000Z", "updatedAt": "2026-06-24T22:00:00.000Z" } ]
Modification d’une Adresse (PATCH /addresses/:id)
- Requête (ex: mise à jour partielle et modification du statut de défaut) :
{ "addressLine2": "Bâtiment C, Appt 14 (Nouveau)", "isDefault": false } - Réponse (
200 OK) :{ "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "userId": "usr-999-default-uuid", "addressLine1": "456 Avenue de la République", "addressLine2": "Bâtiment C, Appt 14 (Nouveau)", "city": "Lyon", "province": "Auvergne-Rhône-Alpes", "country": "France", "postalCode": "69002", "isDefault": false, "createdAt": "2026-06-24T22:00:00.000Z", "updatedAt": "2026-06-24T22:15:00.000Z" }
Suppression d’une Adresse (DELETE /addresses/:id)
- Réponse (
204 No Content) : (Aucun contenu renvoyé)