backend/src/domain/entities/unit.rs
Description et Responsabilités
Le fichier unit.rs définit l’entité de domaine Unit (Lot de copropriété) dans le système KoproGo. Cette entité représente les lots individuels au sein d’une copropriété (appartements, parkings, caves, locaux commerciaux) et gère leurs caractéristiques et leur propriété.
Responsabilités principales:
- Représenter un lot avec ses caractéristiques (type, surface, quote-part) 
- Valider les données lors de la création (numéro, surface, quota) 
- Gérer l’attribution et le retrait de propriétaires 
- Maintenir les métadonnées temporelles (création, mise à jour) 
- Stocker la quote-part (tantièmes) pour le calcul des charges 
Contexte métier:
Dans le droit français de la copropriété, un lot (ou tantième) représente une partie privative d’un immeuble. Chaque lot possède une quote-part exprimée en millièmes qui détermine sa contribution aux charges communes. Les lots peuvent être de différents types (appartements, parkings, caves, commerces) et appartiennent à un ou plusieurs copropriétaires.
Énumérations
UnitType
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UnitType {
    Apartment,
    Parking,
    Cellar,
    Commercial,
    Other,
}
Description:
Énumération représentant les différents types de lots dans une copropriété.
Variantes:
| Variante | Description | 
|---|---|
| 
 | Lot d’habitation (appartement, studio, maison individuelle en copropriété horizontale) | 
| 
 | Place de stationnement (parking couvert, box, garage) | 
| 
 | Cave ou local de rangement | 
| 
 | Local commercial (boutique, bureau, entrepôt) | 
| 
 | Autre type de lot (grenier, comble, jardin privatif, etc.) | 
Traits dérivés:
- Debug: Permet l’affichage pour le débogage
- Clone: Permet la copie de l’énumération
- Serialize: Permet la sérialisation JSON (via serde)
- Deserialize: Permet la désérialisation JSON (via serde)
- PartialEq: Permet la comparaison d’égalité
Utilisation:
let unit_type = UnitType::Apartment;
// Sérialisation JSON
let json = serde_json::to_string(&unit_type).unwrap();
// json = "\"Apartment\""
// Pattern matching
match unit_type {
    UnitType::Apartment => println!("Lot d'habitation"),
    UnitType::Parking => println!("Place de parking"),
    UnitType::Cellar => println!("Cave"),
    UnitType::Commercial => println!("Local commercial"),
    UnitType::Other => println!("Autre type"),
}
Notes:
Cette énumération pourrait être étendue avec des variantes supplémentaires comme:
- Storage: Local de stockage
- Garden: Jardin privatif
- Terrace: Terrasse privative
- Office: Bureau
- Workshop: Atelier
Structures et Types
Unit
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Unit {
    pub id: Uuid,
    pub building_id: Uuid,
    pub unit_number: String,
    pub unit_type: UnitType,
    pub floor: Option<i32>,
    pub surface_area: f64,
    pub quota: f64,
    pub owner_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}
Description:
Structure représentant un lot de copropriété avec toutes ses caractéristiques et son propriétaire.
Champs:
| Champ | Type | Description | 
|---|---|---|
| 
 | 
 | Identifiant unique généré automatiquement (UUID v4) | 
| 
 | 
 | Référence vers l’immeuble auquel appartient le lot | 
| 
 | 
 | Numéro ou identifiant du lot (ex: “A101”, “Cave 12”, “Parking 5”) | 
| 
 | 
 | Type de lot (Apartment, Parking, Cellar, Commercial, Other) | 
| 
 | 
 | Étage du lot (None pour caves, parkings sans étage) | 
| 
 | 
 | Surface en m² (doit être > 0) | 
| 
 | 
 | Quote-part en millièmes (0 < quota ≤ 1000) | 
| 
 | 
 | Référence vers le propriétaire actuel (None si lot vacant) | 
| 
 | 
 | 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ébogage
- Clone: Permet la copie de l’instance
- Serialize: 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:
- Quote-part (quota): Exprimée en millièmes (‰), elle détermine la part de chaque lot dans les charges communes. La somme des quotes-parts de tous les lots d’un immeuble doit égaler 1000 millièmes. 
- Surface: Mesurée selon la loi Carrez pour les lots d’habitation en France (surface plancher ≥ 1,80m de hauteur). 
- Numéro de lot: Format libre permettant diverses conventions de numérotation (ex: “A101” pour Bâtiment A, 1er étage, lot 01). 
- Étage: Optionnel car certains lots (caves, parkings) ne sont pas situés à un étage spécifique. 
- Propriétaire: Optionnel car un lot peut être temporairement vacant (entre deux ventes). 
Méthodes
Unit::new
Signature:
pub fn new(
    building_id: Uuid,
    unit_number: String,
    unit_type: UnitType,
    floor: Option<i32>,
    surface_area: f64,
    quota: f64,
) -> Result<Self, String>
Description:
Constructeur pour créer une nouvelle instance de Unit avec validation des données.
Comportement:
- Valide que - unit_numbern’est pas vide
- Valide que - surface_areaest strictement positive (> 0)
- Valide que - quotaest dans la plage ]0, 1000]
- Génère un nouvel UUID v4 pour - id
- Capture le timestamp actuel UTC pour - created_atet- updated_at
- Initialise - owner_idà- None(lot non attribué)
- Retourne une instance Unit si toutes les validations passent 
- Retourne une erreur descriptive si une validation échoue 
Paramètres:
| Paramètre | Type | Description | 
|---|---|---|
| 
 | 
 | Identifiant de l’immeuble parent (doit exister) | 
| 
 | 
 | Numéro/identifiant du lot (doit être non vide) | 
| 
 | 
 | Type de lot (Apartment, Parking, etc.) | 
| 
 | 
 | Étage du lot (None si non applicable) | 
| 
 | 
 | Surface en m² (doit être > 0.0) | 
| 
 | 
 | Quote-part en millièmes (0.0 < quota ≤ 1000.0) | 
Retour:
- Ok(Unit): Instance Unit valide avec ID généré et timestamps
- Err(String): Message d’erreur descriptif si validation échoue
Erreurs possibles:
- "Unit number cannot be empty": Le numéro de lot est vide
- "Surface area must be greater than 0": La surface est ≤ 0
- "Quota must be between 0 and 1000": La quote-part est ≤ 0 ou > 1000
Exemple d’utilisation:
let building_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
// Création d'un appartement
let apartment = Unit::new(
    building_id,
    "A101".to_string(),
    UnitType::Apartment,
    Some(1),           // 1er étage
    75.5,              // 75.5 m²
    50.0,              // 50 millièmes
).unwrap();
// Création d'une cave (sans étage)
let cellar = Unit::new(
    building_id,
    "Cave 12".to_string(),
    UnitType::Cellar,
    None,              // Pas d'étage
    8.0,               // 8 m²
    2.5,               // 2.5 millièmes
).unwrap();
// Création échouée - surface invalide
let invalid = Unit::new(
    building_id,
    "A102".to_string(),
    UnitType::Apartment,
    Some(1),
    0.0,               // Surface = 0 → ERREUR
    50.0,
);
assert!(invalid.is_err());
assert_eq!(invalid.unwrap_err(), "Surface area must be greater than 0");
// Création échouée - quota invalide
let invalid_quota = Unit::new(
    building_id,
    "A103".to_string(),
    UnitType::Apartment,
    Some(1),
    75.0,
    1500.0,            // Quota > 1000 → ERREUR
);
assert!(invalid_quota.is_err());
assert_eq!(invalid_quota.unwrap_err(), "Quota must be between 0 and 1000");
Cas d’usage métier:
// Immeuble de 10 lots avec répartition des quotes-parts
let building_id = Uuid::new_v4();
// Appartements (80% des quotes-parts = 800‰)
let apt_1 = Unit::new(building_id, "A1".to_string(), UnitType::Apartment, Some(1), 50.0, 100.0).unwrap();
let apt_2 = Unit::new(building_id, "A2".to_string(), UnitType::Apartment, Some(2), 60.0, 120.0).unwrap();
let apt_3 = Unit::new(building_id, "A3".to_string(), UnitType::Apartment, Some(3), 70.0, 140.0).unwrap();
// Parkings (15% = 150‰)
let parking_1 = Unit::new(building_id, "P1".to_string(), UnitType::Parking, Some(-1), 12.5, 50.0).unwrap();
let parking_2 = Unit::new(building_id, "P2".to_string(), UnitType::Parking, Some(-1), 12.5, 50.0).unwrap();
let parking_3 = Unit::new(building_id, "P3".to_string(), UnitType::Parking, Some(-1), 12.5, 50.0).unwrap();
// Caves (5% = 50‰)
let cellar_1 = Unit::new(building_id, "C1".to_string(), UnitType::Cellar, None, 5.0, 25.0).unwrap();
let cellar_2 = Unit::new(building_id, "C2".to_string(), UnitType::Cellar, None, 5.0, 25.0).unwrap();
// Total des quotes-parts = 800 + 150 + 50 = 1000‰ ✓
Unit::assign_owner
Signature:
pub fn assign_owner(&mut self, owner_id: Uuid)
Description:
Attribue un propriétaire à ce lot.
Comportement:
- Définit - owner_idavec l’UUID du propriétaire fourni
- Met à jour - updated_atavec le timestamp actuel UTC
Paramètres:
| Paramètre | Type | Description | 
|---|---|---|
| 
 | 
 | Identifiant du propriétaire à attribuer | 
Retour:
Aucun (()). Méthode mutatrice.
Exemple d’utilisation:
let building_id = Uuid::new_v4();
let mut unit = Unit::new(
    building_id,
    "A101".to_string(),
    UnitType::Apartment,
    Some(1),
    75.5,
    50.0,
).unwrap();
// Initialement, pas de propriétaire
assert_eq!(unit.owner_id, None);
// Attribution d'un propriétaire
let owner_id = Uuid::new_v4();
unit.assign_owner(owner_id);
assert_eq!(unit.owner_id, Some(owner_id));
Cas d’usage métier:
// Scénario: Vente d'un lot
async fn handle_unit_sale(
    unit_id: Uuid,
    new_owner_id: Uuid,
    unit_repo: &impl UnitRepository,
    transaction_repo: &impl TransactionRepository,
) -> Result<(), Error> {
    // 1. Récupérer le lot
    let mut unit = unit_repo.find_by_id(unit_id).await?
        .ok_or(Error::NotFound)?;
    let old_owner_id = unit.owner_id;
    // 2. Attribuer le nouveau propriétaire
    unit.assign_owner(new_owner_id);
    // 3. Sauvegarder
    unit_repo.update(unit).await?;
    // 4. Enregistrer la transaction
    transaction_repo.record_sale(unit_id, old_owner_id, Some(new_owner_id)).await?;
    Ok(())
}
Notes:
- Cette méthode ne valide pas l’existence du propriétaire 
- La validation doit être effectuée au niveau de l’use case ou du repository 
- Le timestamp - updated_atest automatiquement mis à jour pour traçabilité
Unit::remove_owner
Signature:
pub fn remove_owner(&mut self)
Description:
Retire le propriétaire de ce lot (rend le lot vacant).
Comportement:
- Définit - owner_idà- None
- Met à jour - updated_atavec le timestamp actuel UTC
Paramètres:
Aucun
Retour:
Aucun (()). Méthode mutatrice.
Exemple d’utilisation:
let building_id = Uuid::new_v4();
let mut unit = Unit::new(
    building_id,
    "A101".to_string(),
    UnitType::Apartment,
    Some(1),
    75.5,
    50.0,
).unwrap();
// Attribution d'un propriétaire
let owner_id = Uuid::new_v4();
unit.assign_owner(owner_id);
assert_eq!(unit.owner_id, Some(owner_id));
// Retrait du propriétaire
unit.remove_owner();
assert_eq!(unit.owner_id, None);
Cas d’usage métier:
// Scénario: Succession en cours, lot temporairement sans propriétaire désigné
async fn handle_owner_deceased(
    owner_id: Uuid,
    unit_repo: &impl UnitRepository,
) -> Result<(), Error> {
    // 1. Récupérer tous les lots du propriétaire décédé
    let units = unit_repo.find_by_owner_id(owner_id).await?;
    // 2. Retirer le propriétaire de chaque lot
    for mut unit in units {
        unit.remove_owner();
        unit_repo.update(unit).await?;
    }
    // 3. Les lots sont maintenant vacants en attente de succession
    Ok(())
}
// Scénario: Expulsion ou saisie immobilière
async fn handle_unit_seizure(
    unit_id: Uuid,
    unit_repo: &impl UnitRepository,
) -> Result<(), Error> {
    let mut unit = unit_repo.find_by_id(unit_id).await?
        .ok_or(Error::NotFound)?;
    // Retrait du propriétaire suite à saisie
    unit.remove_owner();
    unit_repo.update(unit).await?;
    Ok(())
}
Notes:
- Cette méthode est utile pour les cas de transition (succession, saisie, vente en cours) 
- Un lot sans propriétaire peut poser des questions de gestion des charges 
- Le système devrait avoir une règle métier pour gérer les lots vacants 
Tests
Le fichier contient 3 tests unitaires dans le module tests:
test_create_unit_success
Description:
Vérifie la création réussie d’un Unit avec toutes les données valides.
Ce qui est testé:
#[test]
fn test_create_unit_success() {
    let building_id = Uuid::new_v4();
    let unit = Unit::new(
        building_id,
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        75.5,
        50.0,
    );
    assert!(unit.is_ok());
    let unit = unit.unwrap();
    assert_eq!(unit.unit_number, "A101");
    assert_eq!(unit.surface_area, 75.5);
}
Assertions:
- ✅ La création retourne - Ok
- ✅ Le numéro de lot est correct 
- ✅ La surface est correcte 
test_create_unit_invalid_surface_fails
Description:
Vérifie que la création échoue avec une surface invalide (≤ 0).
Ce qui est testé:
#[test]
fn test_create_unit_invalid_surface_fails() {
    let building_id = Uuid::new_v4();
    let unit = Unit::new(
        building_id,
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        0.0,
        50.0,
    );
    assert!(unit.is_err());
}
Assertions:
- ✅ La création retourne - Erravec surface = 0
test_assign_owner
Description:
Vérifie l’attribution d’un propriétaire à un lot.
Ce qui est testé:
#[test]
fn test_assign_owner() {
    let building_id = Uuid::new_v4();
    let mut unit = Unit::new(
        building_id,
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        75.5,
        50.0,
    )
    .unwrap();
    let owner_id = Uuid::new_v4();
    unit.assign_owner(owner_id);
    assert_eq!(unit.owner_id, Some(owner_id));
}
Assertions:
- ✅ Le propriétaire est correctement attribué 
Couverture des Tests
| Fonctionnalité | Testée | Cas de test | 
|---|---|---|
| Création avec données valides | ✅ | 
 | 
| Validation surface invalide (≤ 0) | ✅ | 
 | 
| Attribution d’un propriétaire | ✅ | 
 | 
| Validation numéro vide | ❌ | Manquant | 
| Validation quota invalide (≤ 0) | ❌ | Manquant | 
| Validation quota invalide (> 1000) | ❌ | Manquant | 
| Retrait d’un propriétaire | ❌ | Manquant | 
| Génération UUID unique | ❌ | Manquant | 
| Timestamps automatiques | ❌ | Manquant | 
| Mise à jour de updated_at | ❌ | Manquant | 
Tests manquants recommandés:
#[test]
fn test_create_unit_empty_number_fails() {
    let result = Unit::new(
        Uuid::new_v4(),
        "".to_string(),
        UnitType::Apartment,
        Some(1),
        75.0,
        50.0,
    );
    assert_eq!(result.unwrap_err(), "Unit number cannot be empty");
}
#[test]
fn test_create_unit_invalid_quota_too_low_fails() {
    let result = Unit::new(
        Uuid::new_v4(),
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        75.0,
        0.0,  // Quota = 0 → invalide
    );
    assert_eq!(result.unwrap_err(), "Quota must be between 0 and 1000");
}
#[test]
fn test_create_unit_invalid_quota_too_high_fails() {
    let result = Unit::new(
        Uuid::new_v4(),
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        75.0,
        1500.0,  // Quota > 1000 → invalide
    );
    assert_eq!(result.unwrap_err(), "Quota must be between 0 and 1000");
}
#[test]
fn test_remove_owner() {
    let mut unit = Unit::new(
        Uuid::new_v4(),
        "A101".to_string(),
        UnitType::Apartment,
        Some(1),
        75.0,
        50.0,
    ).unwrap();
    let owner_id = Uuid::new_v4();
    unit.assign_owner(owner_id);
    assert_eq!(unit.owner_id, Some(owner_id));
    unit.remove_owner();
    assert_eq!(unit.owner_id, None);
}
#[test]
fn test_updated_at_changes_on_assign_owner() {
    let mut unit = Unit::new(...).unwrap();
    let initial_time = unit.updated_at;
    std::thread::sleep(std::time::Duration::from_millis(10));
    unit.assign_owner(Uuid::new_v4());
    assert!(unit.updated_at > initial_time);
}
Architecture et Diagrammes
Relation avec les autres entités
┌─────────────────┐
│  Organization   │
│   (Syndic)      │
└────────┬────────┘
         │
         │ 1:N
         │
┌────────▼────────┐
│    Building     │
│  (Immeuble)     │
└────────┬────────┘
         │
         │ 1:N
         │
┌────────▼────────┐       ┌──────────────┐
│      Unit    ◄──────────┤    Owner     │
│     (Lot)       │   N:1  │(Copropriétaire)│
└────────┬────────┘        └──────────────┘
         │
         │ 1:N
         │
┌────────▼────────┐
│    Expense      │
│   (Charge)      │
└─────────────────┘
Relations:
- Un Unit appartient à un Building (relation N:1 via - building_id)
- Un Unit peut avoir un Owner (relation N:1 via - owner_id)
- Un Owner peut posséder plusieurs Units (relation 1:N) 
- Un Unit génère des Expenses (charges) selon sa quote-part 
Modèle de calcul des charges
[Charge commune totale: 10,000 €]
            │
            ▼
┌───────────────────────────┐
│  Répartition par quote-   │
│  part (millièmes)         │
└───────────┬───────────────┘
            │
            ├─► Unit A (100‰) → 10,000 € × (100/1000) = 1,000 €
            ├─► Unit B (150‰) → 10,000 € × (150/1000) = 1,500 €
            ├─► Unit C (50‰)  → 10,000 € × (50/1000)  = 500 €
            └─► ... (total = 1000‰)
Formule:
Charge du lot = Charge totale × (quota du lot / 1000)
Cycle de vie d’un Unit
[Création]
    │
    ├─► Validation (numéro, surface, quota)
    ├─► Génération UUID
    ├─► owner_id = None (lot vacant)
    │
    ▼
[Lot vacant]
    │
    ├─► assign_owner(owner_id)
    │
    ▼
[Lot attribué]
    │
    ├─► Calcul des charges
    ├─► Association aux dépenses
    ├─► Génération de documents
    │
    ├─► Vente / Transfert
    │   ├─► remove_owner()
    │   └─► assign_owner(new_owner_id)
    │
    ├─► Modification (surface, quota)
    │
    ▼
[Lot actif en copropriété]
Utilisation dans l’Application
Création d’un Unit (Use Case)
Couche Application - Use Case:
// backend/src/application/use_cases/create_unit.rs
pub async fn execute(
    repo: &impl UnitRepository,
    building_repo: &impl BuildingRepository,
    dto: CreateUnitDto,
) -> Result<UnitDto, ApplicationError> {
    // 1. Vérifier que le building existe
    building_repo.find_by_id(dto.building_id).await?
        .ok_or(ApplicationError::NotFound("Building not found".to_string()))?;
    // 2. Créer l'entité Unit (validation automatique)
    let unit = Unit::new(
        dto.building_id,
        dto.unit_number,
        dto.unit_type,
        dto.floor,
        dto.surface_area,
        dto.quota,
    ).map_err(|e| ApplicationError::ValidationError(e))?;
    // 3. Vérifier l'unicité du numéro de lot dans le building
    if repo.exists_by_building_and_number(dto.building_id, &unit.unit_number).await? {
        return Err(ApplicationError::DuplicateError("Unit number already exists".to_string()));
    }
    // 4. Persister dans la base de données
    let saved_unit = repo.create(unit).await?;
    Ok(UnitDto::from(saved_unit))
}
Calcul de la charge d’un Unit
Couche Domain - Service:
// backend/src/domain/services/expense_calculator.rs
pub fn calculate_unit_expense(
    total_expense: f64,
    unit_quota: f64,
) -> f64 {
    // Charge du lot = Charge totale × (quota / 1000)
    total_expense * (unit_quota / 1000.0)
}
// Exemple d'utilisation
let total_expense = 10_000.0;  // 10,000 €
let unit_quota = 50.0;          // 50 millièmes
let unit_expense = calculate_unit_expense(total_expense, unit_quota);
// unit_expense = 500.0 € (10,000 × 50/1000)
Récupération des Units d’un Building
Couche Application - Use Case:
// backend/src/application/use_cases/list_building_units.rs
pub async fn execute(
    repo: &impl UnitRepository,
    building_id: Uuid,
) -> Result<Vec<UnitDto>, ApplicationError> {
    let units = repo.find_by_building_id(building_id).await?;
    Ok(units.into_iter()
        .map(|unit| UnitDto::from(unit))
        .collect())
}
Transfert de propriété
Couche Application - Use Case:
// backend/src/application/use_cases/transfer_unit_ownership.rs
pub async fn execute(
    unit_repo: &impl UnitRepository,
    unit_id: Uuid,
    new_owner_id: Uuid,
) -> Result<UnitDto, ApplicationError> {
    // 1. Récupérer le unit
    let mut unit = unit_repo.find_by_id(unit_id).await?
        .ok_or(ApplicationError::NotFound("Unit not found".to_string()))?;
    // 2. Attribuer le nouveau propriétaire
    unit.assign_owner(new_owner_id);
    // 3. Sauvegarder
    let updated_unit = unit_repo.update(unit).await?;
    Ok(UnitDto::from(updated_unit))
}
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é dépend de:
- Building (via - building_id)
- Owner (via - owner_id)
Elle est utilisée par:
backend/src/domain/entities/unit.rs
        ▲
        │ used by
        │
        ├─► backend/src/application/dto/unit_dto.rs
        ├─► backend/src/application/ports/unit_repository.rs
        ├─► backend/src/application/use_cases/create_unit.rs
        ├─► backend/src/application/use_cases/get_unit.rs
        ├─► backend/src/application/use_cases/list_building_units.rs
        ├─► backend/src/application/use_cases/transfer_unit_ownership.rs
        ├─► backend/src/domain/services/expense_calculator.rs
        ├─► backend/src/infrastructure/repositories/postgres_unit_repository.rs
        └─► backend/src/web/handlers/units.rs
Notes de Conception
Système de Quote-part (Tantièmes)
Principe:
En France, la quote-part est exprimée en millièmes (‰). La somme des quotes-parts de tous les lots d’un immeuble doit toujours égaler 1000 millièmes.
Calcul:
La quote-part est généralement calculée en fonction:
- De la surface du lot (loi Carrez) 
- De la situation (étage, orientation, vue) 
- De la destination (habitation, commerce, parking) 
Exemple:
Immeuble de 3 lots:
- Lot A: 100 m², appartement, 2ème étage → 500‰
- Lot B: 75 m², appartement, 1er étage  → 400‰
- Lot C: 12 m², parking sous-sol        → 100‰
Total: 500 + 400 + 100 = 1000‰ ✓
Utilisation:
// Calcul de la charge annuelle d'un lot
fn calculate_annual_charge(total_building_charges: f64, unit_quota: f64) -> f64 {
    total_building_charges * (unit_quota / 1000.0)
}
// Exemple:
let total_charges = 50_000.0;  // 50,000 € de charges pour l'immeuble
let unit_quota = 50.0;          // Lot avec 50 millièmes
let annual_charge = calculate_annual_charge(total_charges, unit_quota);
// annual_charge = 2,500 € (50,000 × 50/1000)
Validation de la Surface
Problème:
La validation actuelle accepte toute surface > 0, mais ne vérifie pas la cohérence métier.
Améliorations potentielles:
pub fn new(..., surface_area: f64, ...) -> Result<Self, String> {
    // Validation basique
    if surface_area <= 0.0 {
        return Err("Surface area must be greater than 0".to_string());
    }
    // Validation métier selon le type
    match unit_type {
        UnitType::Apartment if surface_area < 9.0 => {
            return Err("Apartment must have at least 9m² (loi Carrez)".to_string());
        }
        UnitType::Parking if surface_area > 50.0 => {
            return Err("Parking cannot exceed 50m²".to_string());
        }
        UnitType::Cellar if surface_area > 30.0 => {
            return Err("Cellar cannot exceed 30m²".to_string());
        }
        _ => {}
    }
    // ...
}
Unicité du Numéro de Lot
Problème:
La méthode new() ne vérifie pas l’unicité du unit_number dans le building.
Solution:
La vérification d’unicité doit être effectuée au niveau du repository ou de l’use case, car elle nécessite un accès à la base de données.
// Dans le repository
async fn exists_by_building_and_number(
    &self,
    building_id: Uuid,
    unit_number: &str,
) -> Result<bool, RepositoryError>;
// Dans l'use case
if repo.exists_by_building_and_number(building_id, &unit_number).await? {
    return Err(ApplicationError::DuplicateError("Unit number already exists".to_string()));
}
Contrainte base de données:
CREATE UNIQUE INDEX idx_units_building_number
ON units(building_id, unit_number);
Gestion des Lots Vacants
Problème:
Un lot sans propriétaire (owner_id = None) pose des questions:
- Qui paie les charges? 
- Comment gérer les assemblées générales? 
- Comment envoyer les documents? 
Solutions:
pub struct Unit {
    // ...
    pub owner_id: Option<Uuid>,
    pub temporary_manager_id: Option<Uuid>,  // Administrateur temporaire
    pub is_vacant: bool,  // Indicateur explicite
}
impl Unit {
    pub fn is_vacant(&self) -> bool {
        self.owner_id.is_none()
    }
    pub fn assign_temporary_manager(&mut self, manager_id: Uuid) {
        self.temporary_manager_id = Some(manager_id);
        self.updated_at = Utc::now();
    }
}
Règles métier:
- Les charges d’un lot vacant peuvent être gérées par le syndic 
- Un administrateur temporaire peut être nommé en cas de succession longue 
Immuabilité des Caractéristiques
Observation:
Tous les champs sont pub (publics et modifiables), permettant des modifications directes non contrôlées.
Recommandation:
pub struct Unit {
    id: Uuid,  // Privé - immuable
    building_id: Uuid,  // Privé - immuable
    unit_number: String,  // Privé - modifiable via méthode
    unit_type: UnitType,  // Privé - immuable (ou très rarement modifiable)
    floor: Option<i32>,  // Privé - immuable
    surface_area: f64,  // Privé - modifiable via méthode (rare)
    quota: f64,  // Privé - modifiable via méthode (assemblée générale)
    owner_id: Option<Uuid>,  // Privé - géré par assign_owner/remove_owner
    created_at: DateTime<Utc>,  // Privé - immuable
    updated_at: DateTime<Utc>,  // Privé - géré automatiquement
}
impl Unit {
    pub fn id(&self) -> Uuid { self.id }
    pub fn unit_number(&self) -> &str { &self.unit_number }
    pub fn quota(&self) -> f64 { self.quota }
    // ... autres getters
    pub fn update_quota(&mut self, new_quota: f64) -> Result<(), String> {
        if new_quota <= 0.0 || new_quota > 1000.0 {
            return Err("Quota must be between 0 and 1000".to_string());
        }
        self.quota = new_quota;
        self.updated_at = Utc::now();
        Ok(())
    }
    pub fn update_surface(&mut self, new_surface: f64) -> Result<(), String> {
        if new_surface <= 0.0 {
            return Err("Surface must be greater than 0".to_string());
        }
        self.surface_area = new_surface;
        self.updated_at = Utc::now();
        Ok(())
    }
}
Avertissements
⚠️ Validation Quote-part Incomplète
La validation actuelle vérifie que quota ≤ 1000, mais ne garantit pas que la somme des quotes-parts de tous les lots d’un building égale 1000.
Recommandation: Implémenter une validation au niveau du Building ou de l’use case.
⚠️ Pas de Validation d’Unicité
Le unit_number peut être dupliqué dans le même building.
Recommandation: Contrainte UNIQUE en base de données + vérification dans l’use case.
⚠️ 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.
⚠️ Pas de Gestion des Lots Vacants
Il n’y a pas de logique métier pour gérer les lots sans propriétaire.
Recommandation: Ajouter temporary_manager_id ou is_vacant flag.
⚠️ Pas de Validation de Cohérence
La surface n’est pas validée selon le type de lot (ex: appartement < 9m²).
Recommandation: Ajouter des règles de validation métier par type.
⚠️ Modification de building_id Possible
Le champ building_id est public et peut être modifié, ce qui n’a aucun sens métier.
Recommandation: Rendre ce champ immuable (privé sans setter).
Fichiers Associés
backend/src/
├── domain/
│   ├── entities/
│   │   ├── unit.rs                     ← CE FICHIER
│   │   ├── building.rs                 (parent)
│   │   └── owner.rs                    (propriétaire)
│   │
│   └── services/
│       └── expense_calculator.rs       (calcul charges)
│
├── application/
│   ├── dto/
│   │   └── unit_dto.rs                 (représentation DTO)
│   │
│   ├── ports/
│   │   └── unit_repository.rs          (trait repository)
│   │
│   └── use_cases/
│       ├── create_unit.rs              (création)
│       ├── get_unit.rs                 (récupération)
│       ├── list_building_units.rs      (listing)
│       └── transfer_unit_ownership.rs  (transfert)
│
├── infrastructure/
│   └── repositories/
│       └── postgres_unit_repository.rs (implémentation PostgreSQL)
│
└── web/
    └── handlers/
        └── units.rs                    (endpoints API REST)
Base de Données (Schema SQL)
-- migrations/XXXXXX_create_units_table.sql
CREATE TABLE units (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    building_id UUID NOT NULL REFERENCES buildings(id) ON DELETE CASCADE,
    unit_number VARCHAR(50) NOT NULL,
    unit_type VARCHAR(50) NOT NULL CHECK (unit_type IN ('Apartment', 'Parking', 'Cellar', 'Commercial', 'Other')),
    floor INTEGER,
    surface_area DOUBLE PRECISION NOT NULL CHECK (surface_area > 0),
    quota DOUBLE PRECISION NOT NULL CHECK (quota > 0 AND quota <= 1000),
    owner_id UUID REFERENCES owners(id) ON DELETE SET NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    -- Contraintes
    CONSTRAINT units_building_number_unique UNIQUE (building_id, unit_number)
);
-- Index pour recherche par building
CREATE INDEX idx_units_building_id ON units(building_id);
-- Index pour recherche par propriétaire
CREATE INDEX idx_units_owner_id ON units(owner_id);
-- Index pour recherche par type
CREATE INDEX idx_units_type ON units(unit_type);
Points d’Amélioration Suggérés
- Validation Quote-part Globale - Vérifier que la somme des quotes-parts d’un building = 1000‰ 
- Champs Privés - Encapsuler les champs et exposer via getters/setters 
- Tests Complets - Ajouter tests pour validation quota, numéro vide, remove_owner 
- Validation Métier par Type - Surface minimale pour appartements (9m² loi Carrez), maximale pour parkings 
- Gestion Lots Vacants - Ajouter - temporary_manager_idou flag- is_vacant
- Historique des Propriétaires - Créer une table - unit_ownership_historypour traçabilité
- Événements de Domaine - Émettre - UnitCreated,- OwnerAssigned,- OwnerRemoved
- Value Objects - Créer - Quotavalue object avec validation encapsulée
- Méthode update_quota - Permettre la modification de la quote-part (assemblée générale) 
- Documentation Inline - Ajouter doc comments Rust ( - ///) pour rustdoc