backend/src/domain/entities/owner.rs
Description et Responsabilités
Le fichier owner.rs définit l’entité de domaine Owner (Copropriétaire) dans le système KoproGo. Cette entité représente les propriétaires d’unités dans une copropriété et gère leurs informations personnelles et de contact.
Responsabilités principales:
Représenter l’identité d’un copropriétaire avec ses informations complètes
Valider les données lors de la création (noms, email)
Gérer les informations de contact (email, téléphone)
Fournir une représentation formatée du nom complet
Maintenir les métadonnées temporelles (création, mise à jour)
Contexte métier:
Un Owner est une personne physique propriétaire d’une ou plusieurs unités (lots) dans une copropriété. Chaque owner possède des informations d’identité complètes (nom, prénom) et de contact (email, téléphone, adresse postale complète). Ces informations sont essentielles pour la communication avec le syndic et pour l’envoi de documents officiels.
Structures et Types
Owner
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Owner {
pub id: Uuid,
pub first_name: String,
pub last_name: String,
pub email: String,
pub phone: Option<String>,
pub address: String,
pub city: String,
pub postal_code: String,
pub country: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Description:
Structure représentant un copropriétaire avec toutes ses informations personnelles et de contact.
Champs:
Champ |
Type |
Description |
|---|---|---|
|
|
Identifiant unique généré automatiquement (UUID v4) |
|
|
Prénom du copropriétaire (obligatoire, non vide) |
|
|
Nom de famille du copropriétaire (obligatoire, non vide) |
|
|
Adresse email de contact (obligatoire, validée) |
|
|
Numéro de téléphone (optionnel) |
|
|
Adresse postale (rue, numéro) |
|
|
Ville de résidence |
|
|
Code postal |
|
|
Pays de résidence |
|
|
Date et heure de création de l’enregistrement |
|
|
Date et heure de dernière mise à jour |
Traits dérivés:
Debug: Permet l’affichage pour le débogageClone: Permet la copie de l’instanceSerialize: Permet la sérialisation JSON (via serde)Deserialize: Permet la désérialisation JSON (via serde)PartialEq: Permet la comparaison d’égalité
Notes de conception:
L’adresse est structurée en plusieurs champs séparés (address, city, postal_code, country) pour faciliter les recherches et le tri géographique
Le téléphone est optionnel car tous les copropriétaires ne souhaitent pas fournir cette information
L’email est obligatoire et validé car c’est le canal principal de communication
Les timestamps permettent de tracer l’historique des modifications
Méthodes
Owner::new
Signature:
pub fn new(
first_name: String,
last_name: String,
email: String,
phone: Option<String>,
address: String,
city: String,
postal_code: String,
country: String,
) -> Result<Self, String>
Description:
Constructeur pour créer une nouvelle instance d’Owner avec validation des données.
Comportement:
Valide que
first_namen’est pas videValide que
last_namen’est pas videValide le format de l’email avec
is_valid_email()Génère un nouvel UUID v4 pour
idCapture le timestamp actuel UTC pour
created_atetupdated_atRetourne une instance Owner si toutes les validations passent
Retourne une erreur descriptive si une validation échoue
Paramètres:
Paramètre |
Type |
Description |
|---|---|---|
|
|
Prénom du copropriétaire (doit être non vide) |
|
|
Nom de famille (doit être non vide) |
|
|
Adresse email (doit contenir @ et .) |
|
|
Numéro de téléphone optionnel |
|
|
Adresse postale complète |
|
|
Ville de résidence |
|
|
Code postal |
|
|
Pays de résidence |
Retour:
Ok(Owner): Instance Owner valide avec ID généré et timestampsErr(String): Message d’erreur descriptif si validation échoue
Erreurs possibles:
"First name cannot be empty": Le prénom est vide"Last name cannot be empty": Le nom est vide"Invalid email format": L’email ne contient pas @ ou .
Exemple d’utilisation:
// Création réussie
let owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
Some("+33612345678".to_string()),
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
).unwrap();
println!("Owner créé: {} (ID: {})", owner.full_name(), owner.id);
// Création échouée - email invalide
let result = Owner::new(
"Marie".to_string(),
"Martin".to_string(),
"invalid-email".to_string(),
None,
"45 Avenue des Champs".to_string(),
"Lyon".to_string(),
"69001".to_string(),
"France".to_string(),
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid email format");
Owner::is_valid_email
Signature:
fn is_valid_email(email: &str) -> bool
Description:
Méthode privée de validation basique du format email.
Comportement:
Vérifie la présence du caractère
@dans l’emailVérifie la présence du caractère
.dans l’emailRetourne
truesi les deux conditions sont remplies
Paramètres:
Paramètre |
Type |
Description |
|---|---|---|
|
|
Chaîne de caractères à valider |
Retour:
bool:truesi l’email contient @ et .,falsesinon
Notes d’implémentation:
Cette validation est délibérément basique et ne suit pas strictement la RFC 5322. Elle accepte les formats suivants:
✅
user@example.com→ valide✅
firstname.lastname@company.co.uk→ valide❌
invalid-email→ invalide (pas de @)❌
missing-domain@→ invalide (pas de .)❌
@no-user.com→ valide (mais incorrecte sémantiquement)
Justification:
Une validation stricte de l’email nécessiterait une bibliothèque externe (comme email_address). Cette validation simple empêche les erreurs de saisie évidentes tout en restant permissive pour les cas edge valides.
Exemple d’utilisation:
assert!(Owner::is_valid_email("user@example.com"));
assert!(Owner::is_valid_email("firstname.lastname@company.co.uk"));
assert!(!Owner::is_valid_email("invalid-email"));
assert!(!Owner::is_valid_email("missing@domain"));
Owner::full_name
Signature:
pub fn full_name(&self) -> String
Description:
Retourne le nom complet formaté du copropriétaire.
Comportement:
Concatène first_name et last_name avec un espace entre les deux.
Paramètres:
Aucun (méthode sur &self)
Retour:
String: Nom complet au format "Prénom Nom"
Exemple d’utilisation:
let owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
None,
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
).unwrap();
assert_eq!(owner.full_name(), "Jean Dupont");
// Utilisation dans un template
println!("Bonjour {}", owner.full_name()); // "Bonjour Jean Dupont"
Notes:
Cette méthode est particulièrement utile pour:
Affichage dans les interfaces utilisateur
Génération de documents (convocations, courriers)
Logs et traces d’audit
Export CSV/Excel
Owner::update_contact
Signature:
pub fn update_contact(&mut self, email: String, phone: Option<String>) -> Result<(), String>
Description:
Met à jour les informations de contact (email et téléphone) avec validation.
Comportement:
Valide le nouveau format email avec
is_valid_email()Si valide, met à jour
emailetphoneMet à jour
updated_atavec le timestamp actuelRetourne
Ok(())si succès,Errsi email invalide
Paramètres:
Paramètre |
Type |
Description |
|---|---|---|
|
|
Nouvelle adresse email (validée) |
|
|
Nouveau numéro de téléphone (peut être None) |
Retour:
Ok(()): Mise à jour effectuée avec succèsErr(String): Erreur de validation avec message
Erreurs possibles:
"Invalid email format": Le nouvel email ne contient pas @ ou .
Exemple d’utilisation:
let mut owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
None,
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
).unwrap();
// Mise à jour réussie
let result = owner.update_contact(
"new.email@example.com".to_string(),
Some("+33699999999".to_string()),
);
assert!(result.is_ok());
assert_eq!(owner.email, "new.email@example.com");
assert_eq!(owner.phone, Some("+33699999999".to_string()));
// Mise à jour échouée - email invalide
let result = owner.update_contact(
"invalid".to_string(),
None,
);
assert!(result.is_err());
assert_eq!(owner.email, "new.email@example.com"); // Inchangé
Notes:
Cette méthode modifie l’instance (
&mut self)Le timestamp
updated_atest automatiquement mis à jourLe téléphone peut être supprimé en passant
NoneL’adresse postale complète ne peut pas être modifiée par cette méthode (nécessiterait une méthode dédiée)
Tests
Le fichier contient 3 tests unitaires dans le module tests:
test_create_owner_success
Description:
Vérifie la création réussie d’un Owner avec toutes les données valides.
Ce qui est testé:
#[test]
fn test_create_owner_success() {
let owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
Some("+33612345678".to_string()),
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
);
assert!(owner.is_ok());
let owner = owner.unwrap();
assert_eq!(owner.full_name(), "Jean Dupont");
}
Assertions:
✅ La création retourne
Ok✅ Le nom complet est correctement formaté
test_create_owner_invalid_email_fails
Description:
Vérifie que la création échoue avec un email invalide.
Ce qui est testé:
#[test]
fn test_create_owner_invalid_email_fails() {
let owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"invalid-email".to_string(),
None,
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
);
assert!(owner.is_err());
assert_eq!(owner.unwrap_err(), "Invalid email format");
}
Assertions:
✅ La création retourne
Err✅ Le message d’erreur est correct
test_update_contact
Description:
Vérifie la mise à jour des informations de contact.
Ce qui est testé:
#[test]
fn test_update_contact() {
let mut owner = Owner::new(
"Jean".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
None,
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
)
.unwrap();
let result = owner.update_contact(
"new.email@example.com".to_string(),
Some("+33699999999".to_string()),
);
assert!(result.is_ok());
assert_eq!(owner.email, "new.email@example.com");
}
Assertions:
✅ La mise à jour retourne
Ok✅ L’email est correctement modifié
Couverture des Tests
Fonctionnalité |
Testée |
Cas de test |
|---|---|---|
Création avec données valides |
✅ |
|
Validation email invalide |
✅ |
|
Mise à jour contact |
✅ |
|
Validation prénom vide |
❌ |
Manquant |
Validation nom vide |
❌ |
Manquant |
Mise à jour avec email invalide |
❌ |
Manquant |
Génération UUID unique |
❌ |
Manquant |
Timestamps automatiques |
❌ |
Manquant |
Tests manquants recommandés:
#[test]
fn test_create_owner_empty_first_name_fails() {
let result = Owner::new(
"".to_string(),
"Dupont".to_string(),
"jean.dupont@example.com".to_string(),
None,
"123 Rue".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
);
assert_eq!(result.unwrap_err(), "First name cannot be empty");
}
#[test]
fn test_create_owner_empty_last_name_fails() {
let result = Owner::new(
"Jean".to_string(),
"".to_string(),
"jean.dupont@example.com".to_string(),
None,
"123 Rue".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
);
assert_eq!(result.unwrap_err(), "Last name cannot be empty");
}
#[test]
fn test_update_contact_invalid_email_fails() {
let mut owner = Owner::new(...).unwrap();
let result = owner.update_contact("invalid".to_string(), None);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid email format");
}
Architecture et Diagrammes
Relation avec les autres entités
┌─────────────────┐
│ Organization │
│ (Syndic) │
└────────┬────────┘
│
│ 1:N
│
┌────────▼────────┐
│ Building │
│ (Immeuble) │
└────────┬────────┘
│
│ 1:N
│
┌────────▼────────┐
│ Unit │
│ (Lot) │
└────────┬────────┘
│
│ N:1
│
┌────────▼────────┐ ┌──────────────┐
│ Owner ◄──────────┤ User │
│ (Copropriétaire)│ 1:1 │ (Compte) │
└─────────────────┘ └──────────────┘
Relations:
Un Owner peut posséder plusieurs Units (relation 1:N)
Un Owner est associé à un User (compte d’authentification)
Les Units appartiennent à des Buildings gérés par une Organization
Cycle de vie d’un Owner
[Création]
│
├─► Validation prénom/nom/email
├─► Génération UUID
├─► Timestamp created_at/updated_at
│
▼
[Owner actif]
│
├─► Mise à jour contact (update_contact)
├─► Association à des Units
├─► Création compte User
├─► Réception documents
├─► Paiement charges
│
▼
[Archivage potentiel]
│
└─► Transfert de propriété
└─► Nouvel Owner créé
Utilisation dans l’Application
Création d’un Owner (Use Case)
Couche Application - Use Case:
// backend/src/application/use_cases/create_owner.rs
pub async fn execute(
repo: &impl OwnerRepository,
dto: CreateOwnerDto,
) -> Result<OwnerDto, ApplicationError> {
// 1. Créer l'entité Owner (validation automatique)
let owner = Owner::new(
dto.first_name,
dto.last_name,
dto.email,
dto.phone,
dto.address,
dto.city,
dto.postal_code,
dto.country,
).map_err(|e| ApplicationError::ValidationError(e))?;
// 2. Persister dans la base de données
let saved_owner = repo.create(owner).await?;
// 3. Retourner le DTO
Ok(OwnerDto::from(saved_owner))
}
Flux complet:
[API Handler]
│
├─► Reçoit CreateOwnerDto
│
▼
[Create Owner Use Case]
│
├─► Owner::new() → Validation
├─► OwnerRepository::create() → Persistence
│
▼
[Owner créé et retourné]
Récupération d’un Owner
Couche Application - Use Case:
// backend/src/application/use_cases/get_owner.rs
pub async fn execute(
repo: &impl OwnerRepository,
owner_id: Uuid,
) -> Result<OwnerDto, ApplicationError> {
let owner = repo.find_by_id(owner_id).await?
.ok_or(ApplicationError::NotFound("Owner not found".to_string()))?;
Ok(OwnerDto::from(owner))
}
Mise à jour des coordonnées
Couche Application - Use Case:
// backend/src/application/use_cases/update_owner_contact.rs
pub async fn execute(
repo: &impl OwnerRepository,
owner_id: Uuid,
email: String,
phone: Option<String>,
) -> Result<OwnerDto, ApplicationError> {
// 1. Récupérer l'owner
let mut owner = repo.find_by_id(owner_id).await?
.ok_or(ApplicationError::NotFound("Owner not found".to_string()))?;
// 2. Mettre à jour (validation automatique)
owner.update_contact(email, phone)
.map_err(|e| ApplicationError::ValidationError(e))?;
// 3. Persister les changements
let updated_owner = repo.update(owner).await?;
Ok(OwnerDto::from(updated_owner))
}
Notes importantes:
Le timestamp updated_at est automatiquement mis à jour par la méthode update_contact(), garantissant la traçabilité des modifications.
Dépendances
Dépendances Externes
use chrono::{DateTime, Utc}; // Gestion des dates et timestamps UTC
use serde::{Deserialize, Serialize}; // Sérialisation/désérialisation JSON
use uuid::Uuid; // Génération et manipulation d'UUID v4
Dépendances Internes
Cette entité est autonome et ne dépend d’aucune autre entité de domaine. Elle est utilisée par:
backend/src/domain/entities/owner.rs
▲
│ used by
│
├─► backend/src/application/dto/owner_dto.rs
├─► backend/src/application/ports/owner_repository.rs
├─► backend/src/application/use_cases/create_owner.rs
├─► backend/src/application/use_cases/get_owner.rs
├─► backend/src/application/use_cases/update_owner_contact.rs
├─► backend/src/infrastructure/repositories/postgres_owner_repository.rs
└─► backend/src/web/handlers/owners.rs
Notes de Conception
Validation des Données
Principe:
La validation est effectuée à la création et lors des mises à jour pour garantir l’intégrité des données.
Règles de validation:
✅ Prénom non vide (business rule)
✅ Nom non vide (business rule)
✅ Email contient @ et . (validation basique)
⚠️ Téléphone optionnel (pas de validation de format)
⚠️ Adresse/ville/code postal non validés (acceptent chaînes vides)
Améliorations potentielles:
// Validation stricte de l'email avec bibliothèque externe
use email_address::EmailAddress;
fn is_valid_email(email: &str) -> bool {
EmailAddress::is_valid(email)
}
// Validation du format téléphone international
use phonenumber::PhoneNumber;
fn is_valid_phone(phone: &str) -> bool {
PhoneNumber::parse(None, phone).is_ok()
}
// Validation code postal français
fn is_valid_french_postal_code(code: &str) -> bool {
code.len() == 5 && code.chars().all(|c| c.is_digit(10))
}
Immuabilité Partielle
Design actuel:
Tous les champs sont
pub(publics et modifiables)Seule
update_contact()offre une mise à jour contrôlée
Alternative recommandée:
pub struct Owner {
id: Uuid, // Privé - immuable
first_name: String, // Privé
last_name: String, // Privé
email: String, // Privé - modifiable via update_contact
phone: Option<String>, // Privé - modifiable via update_contact
address: String, // Privé - modifiable via update_address
// ...
created_at: DateTime<Utc>, // Privé - immuable
updated_at: DateTime<Utc>, // Privé - géré automatiquement
}
impl Owner {
pub fn id(&self) -> Uuid { self.id }
pub fn first_name(&self) -> &str { &self.first_name }
pub fn last_name(&self) -> &str { &self.last_name }
pub fn email(&self) -> &str { &self.email }
// ... autres getters
pub fn update_name(&mut self, first: String, last: String) -> Result<(), String> {
// Validation + mise à jour
}
pub fn update_address(&mut self, address: String, city: String, postal: String, country: String) -> Result<(), String> {
// Mise à jour adresse complète
}
}
Avantages:
Encapsulation complète des données
Contrôle strict des modifications
Traçabilité via
updated_atImpossible de modifier
idoucreated_at
Gestion des Timestamps
Comportement actuel:
created_at: Défini à la création, jamais modifiéupdated_at: Défini à la création, mis à jour parupdate_contact()
Limitation:
D’autres modifications (changement de nom, adresse) ne mettent pas à jour updated_at automatiquement.
Solution:
impl Owner {
fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn update_name(&mut self, first: String, last: String) -> Result<(), String> {
// Validation...
self.first_name = first;
self.last_name = last;
self.touch(); // Mise à jour automatique
Ok(())
}
pub fn update_contact(&mut self, email: String, phone: Option<String>) -> Result<(), String> {
// Validation...
self.email = email;
self.phone = phone;
self.touch(); // Mise à jour automatique
Ok(())
}
}
Avertissements
⚠️ Validation Email Basique
La validation email actuelle est trop permissive et accepte des formats invalides comme @domain.com (pas d’utilisateur) ou user@ (pas de domaine complet).
Recommandation: Utiliser une bibliothèque de validation stricte en production.
⚠️ Pas de Validation de Duplicata
La méthode new() ne vérifie pas l’unicité de l’email. Deux owners peuvent avoir le même email.
Recommandation: Implémenter une contrainte UNIQUE en base de données et gérer l’erreur au niveau du repository.
⚠️ Champs Publics
Tous les champs sont publics, permettant des modifications directes non contrôlées.
Recommandation: Rendre les champs privés et exposer via getters/setters avec validation.
⚠️ Pas de Validation d’Adresse
Les champs address, city, postal_code acceptent des chaînes vides.
Recommandation: Ajouter des validations spécifiques selon le pays.
⚠️ Pas de Gestion de Soft Delete
Il n’y a pas de champ deleted_at ou is_active pour gérer la désactivation d’un owner.
Recommandation: Ajouter un champ is_active: bool ou deleted_at: Option<DateTime<Utc>> pour le soft delete.
Fichiers Associés
backend/src/
├── domain/
│ └── entities/
│ ├── owner.rs ← CE FICHIER
│ ├── unit.rs (référence Owner)
│ └── user.rs (lié à Owner)
│
├── application/
│ ├── dto/
│ │ └── owner_dto.rs (représentation DTO)
│ │
│ ├── ports/
│ │ └── owner_repository.rs (trait repository)
│ │
│ └── use_cases/
│ ├── create_owner.rs (création)
│ ├── get_owner.rs (récupération)
│ ├── update_owner_contact.rs (mise à jour)
│ └── list_owners.rs (listing)
│
├── infrastructure/
│ └── repositories/
│ └── postgres_owner_repository.rs (implémentation PostgreSQL)
│
└── web/
└── handlers/
└── owners.rs (endpoints API REST)
Base de Données (Schema SQL)
-- migrations/XXXXXX_create_owners_table.sql
CREATE TABLE owners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(50),
address TEXT NOT NULL,
city VARCHAR(255) NOT NULL,
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Contraintes
CONSTRAINT owners_email_unique UNIQUE (email),
CONSTRAINT owners_first_name_not_empty CHECK (LENGTH(first_name) > 0),
CONSTRAINT owners_last_name_not_empty CHECK (LENGTH(last_name) > 0),
CONSTRAINT owners_email_format CHECK (email LIKE '%@%.%')
);
-- Index pour recherche par email
CREATE INDEX idx_owners_email ON owners(email);
-- Index pour recherche par nom
CREATE INDEX idx_owners_name ON owners(last_name, first_name);
-- Index pour recherche géographique
CREATE INDEX idx_owners_location ON owners(country, city);
Points d’Amélioration Suggérés
Validation Email Stricte
Remplacer la validation basique par une bibliothèque robuste
Champs Privés
Encapsuler les champs et exposer via getters/setters
Tests Complets
Ajouter les tests manquants (nom vide, email invalide en update)
Validation Téléphone
Valider le format international du téléphone
Validation Adresse
Valider les champs d’adresse selon le pays
Soft Delete
Ajouter un champ
is_activeoudeleted_atMéthode update_address
Créer une méthode dédiée pour mettre à jour l’adresse complète
Événements de Domaine
Émettre des événements lors de la création/modification (
OwnerCreated,OwnerContactUpdated)Value Objects
Créer des Value Objects pour
Email,PhoneNumber,AddressDocumentation Inline
Ajouter des doc comments Rust (
///) pour générer la documentation avec rustdoc