backend/src/domain/entities/building.rs
Description
Entité domaine représentant un immeuble en copropriété dans le système Koprogo. Cette entité encapsule toutes les informations géographiques et structurelles d’un immeuble géré par un syndic.
Responsabilités
Modélisation d’immeuble - Informations d’identification (nom, adresse) - Localisation géographique complète - Caractéristiques structurelles (nombre de lots, année de construction)
Validation métier - Nom d’immeuble non vide - Nombre de lots strictement positif - Intégrité des données
Gestion du cycle de vie - Création avec validation - Mise à jour des informations - Tracking des modifications (timestamps)
Structures
Building
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Building {
pub id: Uuid,
pub name: String,
pub address: String,
pub city: String,
pub postal_code: String,
pub country: String,
pub total_units: i32,
pub construction_year: Option<i32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Description:
Représente un immeuble en copropriété avec sa localisation complète et ses caractéristiques.
Champs:
Champ |
Type |
Description |
|---|---|---|
|
|
Identifiant unique UUID v4 |
|
|
Nom de l’immeuble ou de la résidence (non vide) |
|
|
Adresse complète (numéro et rue) |
|
|
Ville de localisation |
|
|
Code postal (format libre pour support international) |
|
|
Pays de localisation |
|
|
Nombre total de lots dans l’immeuble (> 0) |
|
|
Année de construction (optionnelle si inconnue) |
|
|
Date de création de l’enregistrement (UTC) |
|
|
Date de dernière modification (UTC) |
Contraintes métier:
namene peut pas être videtotal_unitsdoit être > 0 (au moins 1 lot)construction_yearpeut êtreNonesi inconnuLes timestamps sont automatiquement gérés
Méthodes
new()
Signature:
pub fn new(
name: String,
address: String,
city: String,
postal_code: String,
country: String,
total_units: i32,
construction_year: Option<i32>,
) -> Result<Self, String>
Description:
Constructeur qui crée un nouvel immeuble avec validation des règles métier.
Comportement:
Valide que
namen’est pas videValide que
total_unitsest strictement positifGénère un UUID v4 unique
Initialise
created_atetupdated_atàUtc::now()
Paramètres:
name- Nom de l’immeuble (ex: “Résidence Les Jardins”)address- Adresse complète (ex: “123 Rue de la Paix”)city- Ville (ex: “Paris”)postal_code- Code postal (ex: “75001”)country- Pays (ex: “France”)total_units- Nombre de lots (doit être > 0)construction_year- Année de construction (optionnelle)
Retour:
Ok(Building)- Immeuble créé avec succèsErr(String)- Message d’erreur de validation
Erreurs possibles:
Condition |
Message d’erreur |
|---|---|
|
“Building name cannot be empty” |
|
“Total units must be greater than 0” |
Exemples:
use uuid::Uuid;
use chrono::Utc;
// ✅ Création réussie
let building = Building::new(
"Résidence Les Champs".to_string(),
"15 Avenue des Champs-Élysées".to_string(),
"Paris".to_string(),
"75008".to_string(),
"France".to_string(),
50, // 50 lots
Some(1985),
);
assert!(building.is_ok());
let building = building.unwrap();
assert_eq!(building.name, "Résidence Les Champs");
assert_eq!(building.total_units, 50);
assert!(building.construction_year == Some(1985));
// ✅ Sans année de construction
let building = Building::new(
"Résidence Moderne".to_string(),
"10 Rue de la République".to_string(),
"Lyon".to_string(),
"69001".to_string(),
"France".to_string(),
30,
None, // Année inconnue
);
assert!(building.is_ok());
// ❌ Nom vide
let building = Building::new(
"".to_string(), // Invalide
"123 Rue Test".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
10,
None,
);
assert!(building.is_err());
assert_eq!(building.unwrap_err(), "Building name cannot be empty");
// ❌ Nombre de lots invalide
let building = Building::new(
"Résidence Test".to_string(),
"123 Rue Test".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
0, // Invalide
None,
);
assert!(building.is_err());
assert_eq!(building.unwrap_err(), "Total units must be greater than 0");
update_info()
Signature:
pub fn update_info(
&mut self,
name: String,
address: String,
city: String,
postal_code: String,
)
Description:
Met à jour les informations principales de l’immeuble.
Comportement:
Met à jour les champs
name,address,city,postal_codeMet à jour
updated_atàUtc::now()Ne modifie PAS
country,total_units,construction_year(données structurelles)
Paramètres:
name- Nouveau nom de l’immeubleaddress- Nouvelle adressecity- Nouvelle villepostal_code- Nouveau code postal
Note:
Cette méthode ne valide pas les données (pas de Result). La validation doit être faite au niveau supérieur (use case) si nécessaire.
Exemple:
let mut building = Building::new(
"Ancien Nom".to_string(),
"Ancienne Adresse".to_string(),
"Ancienne Ville".to_string(),
"00000".to_string(),
"France".to_string(),
20,
None,
).unwrap();
let old_updated_at = building.updated_at;
// Mise à jour
building.update_info(
"Nouveau Nom".to_string(),
"Nouvelle Adresse".to_string(),
"Nouvelle Ville".to_string(),
"75001".to_string(),
);
// Vérifications
assert_eq!(building.name, "Nouveau Nom");
assert_eq!(building.address, "Nouvelle Adresse");
assert_eq!(building.city, "Nouvelle Ville");
assert_eq!(building.postal_code, "75001");
// Le timestamp est mis à jour
assert!(building.updated_at > old_updated_at);
// Les données structurelles ne changent pas
assert_eq!(building.total_units, 20);
assert_eq!(building.country, "France");
Cas d’usage typiques
Création d’un immeuble
// Dans un use case ou handler
let building = Building::new(
"Résidence Les Jardins".to_string(),
"123 Rue de la Paix".to_string(),
"Paris".to_string(),
"75001".to_string(),
"France".to_string(),
45,
Some(1990),
)?;
// Sauvegarder via repository
let saved_building = building_repository.create(building).await?;
Recherche d’immeubles
// Par ville
let buildings = building_repository
.find_by_city("Paris")
.await?;
// Par ID
let building = building_repository
.find_by_id(building_id)
.await?;
Mise à jour d’informations
// Récupérer l'immeuble
let mut building = building_repository
.find_by_id(building_id)
.await?;
// Mettre à jour
building.update_info(
"Nouveau Nom".to_string(),
"Nouvelle Adresse".to_string(),
"Paris".to_string(),
"75002".to_string(),
);
// Sauvegarder
building_repository.update(building).await?;
Tests unitaires
Le fichier contient 4 tests unitaires couvrant:
Test |
Scénario couvert |
|---|---|
|
Création réussie avec toutes les données |
|
Rejet nom vide |
|
Rejet nombre de lots = 0 |
|
Mise à jour informations + timestamp |
Exécuter les tests:
cd backend
cargo test domain::entities::building
Modèle de données
Schéma de base de données PostgreSQL:
CREATE TABLE buildings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL CHECK (length(name) > 0),
address VARCHAR(255) NOT NULL,
city VARCHAR(100) NOT NULL,
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(100) NOT NULL,
total_units INTEGER NOT NULL CHECK (total_units > 0),
construction_year INTEGER,
organization_id UUID REFERENCES organizations(id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_buildings_city ON buildings(city);
CREATE INDEX idx_buildings_org ON buildings(organization_id);
Mapping Rust ↔ PostgreSQL:
Rust |
PostgreSQL |
Notes |
|---|---|---|
|
|
Génération côté Rust (v4) |
|
|
Validation longueur côté Rust |
|
|
Contrainte CHECK en base |
|
|
Nullable en base |
|
|
Toujours UTC |
Relations avec autres entités
Un immeuble (Building) est lié à:
Building
│
├──> Organization (1:1) - Appartient à une organisation
│
├──> Units (1:N) - Contient plusieurs lots
│
├──> Owners (N:M via Units) - Propriétaires via les lots
│
├──> Expenses (1:N) - Charges de l'immeuble
│
├──> Meetings (1:N) - Assemblées générales
│
└──> Documents (1:N) - Documents liés à l'immeuble
Exemple relationnel:
// Récupérer immeuble avec tous ses lots
let building = building_repository.find_by_id(id).await?;
let units = unit_repository.find_by_building_id(building.id).await?;
// Calculer occupation
let occupied_units = units.iter().filter(|u| u.owner_id.is_some()).count();
let occupancy_rate = (occupied_units as f64 / building.total_units as f64) * 100.0;
Intégration Multi-tenant
L’entité Building est multi-tenant via le champ organization_id (dans la base de données, pas exposé dans cette struct simplifiée).
┌─────────────────────────────────────────────────────────┐
│ Organization A (Syndic Paris) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Building 1 │ │ Building 2 │ │ Building 3 │ │
│ │ 75001 │ │ 75002 │ │ 75003 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Organization B (Syndic Lyon) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Building 4 │ │ Building 5 │ │
│ │ 69001 │ │ 69002 │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
Dépendances
Crates externes:
uuid- Identifiants uniqueschrono- Gestion des dates et timestampsserde- Sérialisation/désérialisation JSON
Modules internes:
Aucun (entité auto-suffisante sans dépendances internes)
Évolutions possibles
Validation améliorée
use validator::Validate; #[derive(Validate)] pub struct Building { #[validate(length(min = 3, max = 255))] pub name: String, #[validate(range(min = 1, max = 10000))] pub total_units: i32, #[validate(range(min = 1800, max = 2100))] pub construction_year: Option<i32>, // ... }
Adresse structurée
pub struct Address { pub street_number: String, pub street_name: String, pub city: String, pub postal_code: String, pub country: Country, // Enum } pub struct Building { // ... pub address: Address, // ... }
Géolocalisation
pub struct GeoCoordinates { pub latitude: f64, pub longitude: f64, } pub struct Building { // ... pub coordinates: Option<GeoCoordinates>, // ... }
Métadonnées étendues
pub struct Building { // ... pub building_type: BuildingType, // Enum: Residential, Commercial, Mixed pub floors_count: Option<i32>, pub has_elevator: bool, pub has_parking: bool, pub cadastral_reference: Option<String>, // ... }
Fichiers associés
backend/src/domain/entities/unit.rs- Entité Lot (Unit)backend/src/domain/entities/owner.rs- Entité Propriétairebackend/src/domain/entities/expense.rs- Entité Chargebackend/src/application/ports/building_repository.rs- Trait repositorybackend/src/infrastructure/database/repositories/building_repository_impl.rs- Implémentation PostgreSQLbackend/src/application/use_cases/building_use_cases.rs- Cas d’usage métierbackend/src/application/dto/building_dto.rs- DTOs pour APIbackend/src/infrastructure/web/handlers/building_handlers.rs- Handlers HTTP