backend/src/domain/entities/organization.rs
Description
Entité domaine représentant une organisation (syndic de copropriété) dans la plateforme Koprogo. Cette entité implémente un système de gestion multi-tenant avec différents plans d’abonnement et des limites de ressources associées.
Responsabilités
Modélisation organisation - Identité et informations de contact - Gestion du plan d’abonnement - Limites de ressources (immeubles, utilisateurs)
Validation métier - Validation de l’email de contact - Validation du nom (longueur minimale) - Normalisation des données (trim, lowercase pour email)
Logique métier - Génération automatique de slug URL-friendly - Contrôle des limites de ressources par plan - Activation/désactivation de l’organisation - Mise à niveau du plan d’abonnement
Énumérations
SubscriptionPlan
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SubscriptionPlan {
Free,
Starter,
Professional,
Enterprise,
}
Description:
Énumération des plans d’abonnement disponibles avec des limites de ressources associées.
Variantes:
Plan |
Max Immeubles |
Max Utilisateurs |
Description |
|---|---|---|---|
|
1 |
3 |
Plan gratuit pour petites copropriétés (essai) |
|
5 |
10 |
Plan de démarrage pour syndics indépendants |
|
20 |
50 |
Plan professionnel pour syndics établis |
|
Illimité |
Illimité |
Plan entreprise pour grands groupes immobiliers |
Traits implémentés:
Display- Conversion en chaîne lowercase (ex: “free”, “professional”)FromStr- Parsing depuis chaîne avec gestion d’erreursSerialize/Deserialize- Sérialisation JSON via SerdePartialEq/Eq- Comparaison
Exemples:
use std::str::FromStr;
// Conversion en String
let plan = SubscriptionPlan::Professional;
assert_eq!(plan.to_string(), "professional");
// Parsing depuis String
let plan = SubscriptionPlan::from_str("starter").unwrap();
assert_eq!(plan, SubscriptionPlan::Starter);
// Erreur pour plan invalide
let result = SubscriptionPlan::from_str("invalid");
assert!(result.is_err());
Structures
Organization
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct Organization {
pub id: Uuid,
#[validate(length(min = 2, message = "Name must be at least 2 characters"))]
pub name: String,
pub slug: String,
#[validate(email(message = "Contact email must be valid"))]
pub contact_email: String,
pub contact_phone: Option<String>,
pub subscription_plan: SubscriptionPlan,
pub max_buildings: i32,
pub max_users: i32,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Description:
Représente une organisation (syndic de copropriété) avec ses informations de contact, son plan d’abonnement et ses limites de ressources.
Champs:
Champ |
Type |
Description |
|---|---|---|
|
|
Identifiant unique UUID v4 |
|
|
Nom de l’organisation (minimum 2 caractères) |
|
|
Identifiant URL-friendly (généré automatiquement depuis le nom) |
|
|
Email de contact (validé, normalisé en lowercase) |
|
|
Numéro de téléphone optionnel |
|
|
Plan d’abonnement actuel |
|
|
Nombre maximum d’immeubles autorisés (déterminé par le plan) |
|
|
Nombre maximum d’utilisateurs autorisés (déterminé par le plan) |
|
|
Indicateur d’activation de l’organisation |
|
|
Date de création (UTC) |
|
|
Date de dernière modification (UTC) |
Validations automatiques:
Email: Format RFC 5322
Nom: Longueur >= 2 caractères
Email normalisé: trim() + to_lowercase()
Nom normalisé: trim()
Slug: Généré automatiquement (alphanumeric + tirets)
Méthodes
new()
Signature:
pub fn new(
name: String,
contact_email: String,
contact_phone: Option<String>,
subscription_plan: SubscriptionPlan,
) -> Result<Self, String>
Description:
Constructeur qui crée une nouvelle organisation avec validation automatique et configuration du plan d’abonnement.
Comportement:
Génère un UUID v4 unique
Normalise le nom (trim)
Génère un slug URL-friendly depuis le nom
Normalise l’email (lowercase + trim)
Détermine les limites (max_buildings, max_users) selon le plan
Active l’organisation par défaut (
is_active = true)Initialise les timestamps à
Utc::now()Exécute les validations (email format, longueur nom)
Paramètres:
name- Nom de l’organisationcontact_email- Email de contact (sera normalisé)contact_phone- Numéro de téléphone optionnelsubscription_plan- Plan d’abonnement initial
Retour:
Ok(Organization)- Organisation créée avec succèsErr(String)- Message d’erreur de validation
Exemples:
// ✅ Création réussie avec plan Professional
let org = Organization::new(
"Syndic Immobilier Paris".to_string(),
"contact@syndic-paris.fr".to_string(),
Some("+33123456789".to_string()),
SubscriptionPlan::Professional,
);
assert!(org.is_ok());
let org = org.unwrap();
assert_eq!(org.name, "Syndic Immobilier Paris");
assert_eq!(org.slug, "syndic-immobilier-paris");
assert_eq!(org.max_buildings, 20);
assert_eq!(org.max_users, 50);
// ✅ Slug généré avec caractères spéciaux
let org = Organization::new(
"My Super Company!!!".to_string(),
"contact@example.com".to_string(),
None,
SubscriptionPlan::Free,
).unwrap();
assert_eq!(org.slug, "my-super-company");
// ❌ Email invalide
let result = Organization::new(
"Test Company".to_string(),
"invalid-email".to_string(),
None,
SubscriptionPlan::Starter,
);
assert!(result.is_err());
generate_slug() (privée)
Signature:
fn generate_slug(name: &str) -> String
Description:
Génère un slug URL-friendly à partir du nom de l’organisation.
Algorithme:
Convertit en lowercase
Remplace les caractères non-alphanumériques par
-Supprime les tirets consécutifs
Supprime les tirets en début/fin
Exemples:
// "Company Name" → "company-name"
// "Café-Bar & Restaurant!" → "caf-bar-restaurant"
// "123 Main Street" → "123-main-street"
get_limits_for_plan() (privée)
Signature:
fn get_limits_for_plan(plan: &SubscriptionPlan) -> (i32, i32)
Description:
Retourne les limites (max_buildings, max_users) pour un plan donné.
Retour:
Tuple (max_buildings, max_users)
Limites par plan:
Free → (1, 3)
Starter → (5, 10)
Professional → (20, 50)
Enterprise → (i32::MAX, i32::MAX)
upgrade_plan()
Signature:
pub fn upgrade_plan(&mut self, new_plan: SubscriptionPlan)
Description:
Met à niveau (ou rétrograde) le plan d’abonnement de l’organisation et ajuste automatiquement les limites de ressources.
Comportement:
Modifie
subscription_planRecalcule
max_buildingsetmax_usersMet à jour
updated_at
Paramètres:
new_plan- Nouveau plan d’abonnement
Exemple:
let mut org = Organization::new(
"Test Org".to_string(),
"test@test.com".to_string(),
None,
SubscriptionPlan::Free,
).unwrap();
assert_eq!(org.max_buildings, 1);
assert_eq!(org.max_users, 3);
org.upgrade_plan(SubscriptionPlan::Professional);
assert_eq!(org.subscription_plan, SubscriptionPlan::Professional);
assert_eq!(org.max_buildings, 20);
assert_eq!(org.max_users, 50);
update_contact()
Signature:
pub fn update_contact(&mut self, email: String, phone: Option<String>) -> Result<(), String>
Description:
Met à jour les informations de contact de l’organisation avec validation.
Comportement:
Normalise le nouvel email (lowercase + trim)
Met à jour
contact_emailetcontact_phoneMet à jour
updated_atValide les nouvelles valeurs
Paramètres:
email- Nouveau email de contactphone- Nouveau numéro de téléphone (optionnel)
Retour:
Ok(())- Mise à jour réussieErr(String)- Erreur de validation (email invalide)
Exemple:
let mut org = Organization::new(/* ... */).unwrap();
let result = org.update_contact(
"new-contact@example.com".to_string(),
Some("+33987654321".to_string()),
);
assert!(result.is_ok());
assert_eq!(org.contact_email, "new-contact@example.com");
deactivate()
Signature:
pub fn deactivate(&mut self)
Description:
Désactive l’organisation. Une organisation désactivée ne peut plus ajouter d’immeubles ou d’utilisateurs.
Comportement:
Définit
is_activeàfalseMet à jour
updated_at
Exemple:
let mut org = Organization::new(/* ... */).unwrap();
assert!(org.is_active);
org.deactivate();
assert!(!org.is_active);
assert!(!org.can_add_building(0));
assert!(!org.can_add_user(0));
activate()
Signature:
pub fn activate(&mut self)
Description:
Réactive une organisation précédemment désactivée.
Comportement:
Définit
is_activeàtrueMet à jour
updated_at
Exemple:
let mut org = Organization::new(/* ... */).unwrap();
org.deactivate();
org.activate();
assert!(org.is_active);
can_add_building()
Signature:
pub fn can_add_building(&self, current_count: i32) -> bool
Description:
Vérifie si l’organisation peut ajouter un nouvel immeuble selon son plan d’abonnement et son statut.
Logique:
┌─────────────────────────────────────────────────┐
│ Organisation active ? │
│ ├─ Non → ❌ Refusé │
│ └─ Oui → Vérifier limites │
│ └─ current_count < max_buildings ? │
│ ├─ Oui → ✅ Autorisé │
│ └─ Non → ❌ Limite atteinte │
└─────────────────────────────────────────────────┘
Paramètres:
current_count- Nombre actuel d’immeubles de l’organisation
Retour:
true- Ajout autoriséfalse- Limite atteinte ou organisation désactivée
Exemples:
// Plan Starter: max 5 immeubles
let org = Organization::new(
"Test Org".to_string(),
"test@test.com".to_string(),
None,
SubscriptionPlan::Starter,
).unwrap();
assert!(org.can_add_building(0)); // ✅ 0 < 5
assert!(org.can_add_building(4)); // ✅ 4 < 5
assert!(!org.can_add_building(5)); // ❌ 5 >= 5
// Organisation désactivée
let mut org = org;
org.deactivate();
assert!(!org.can_add_building(0)); // ❌ Inactif
can_add_user()
Signature:
pub fn can_add_user(&self, current_count: i32) -> bool
Description:
Vérifie si l’organisation peut ajouter un nouvel utilisateur selon son plan d’abonnement et son statut.
Logique:
Identique à can_add_building() mais compare avec max_users.
Paramètres:
current_count- Nombre actuel d’utilisateurs de l’organisation
Retour:
true- Ajout autoriséfalse- Limite atteinte ou organisation désactivée
Exemple:
// Plan Free: max 3 utilisateurs
let org = Organization::new(
"Test Org".to_string(),
"test@test.com".to_string(),
None,
SubscriptionPlan::Free,
).unwrap();
assert!(org.can_add_user(0)); // ✅ 0 < 3
assert!(org.can_add_user(2)); // ✅ 2 < 3
assert!(!org.can_add_user(3)); // ❌ 3 >= 3
Tests unitaires
Le fichier contient 6 tests unitaires couvrant:
Test |
Scénario couvert |
|---|---|
|
Création réussie avec données valides |
|
Génération de slug avec caractères spéciaux |
|
Vérification des limites par plan |
|
Mise à niveau de plan |
|
Vérification des limites d’immeubles |
|
Organisation désactivée ne peut rien ajouter |
Exécuter les tests:
cd backend
cargo test domain::entities::organization
Architecture Multi-tenant
L’entité Organization est au cœur du système multi-tenant de Koprogo:
┌─────────────────────────────────────────────────┐
│ Organization 1 (Free) │
│ ┌──────────────┐ │
│ │ Building A │ │
│ └──────────────┘ │
│ Users: Syndic, Owner1, Owner2 (max 3) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Organization 2 (Professional) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Building A │ │ Building B │ │
│ └──────────────┘ └──────────────┘ │
│ ...jusqu'à 20 immeubles... │
│ Users: jusqu'à 50 utilisateurs │
└─────────────────────────────────────────────────┘
Matrice des plans d’abonnement
Fonctionnalité |
Free |
Starter |
Professional |
|---|---|---|---|
Immeubles |
1 |
5 |
20 |
Utilisateurs |
3 |
10 |
50 |
Cas d’usage |
Petite copropriété |
Syndic indépendant |
Cabinet immobilier |
Prix suggéré |
0€/mois |
49€/mois |
199€/mois |
Note
Le plan Enterprise offre des ressources illimitées (i32::MAX) et est destiné aux grands groupes immobiliers avec des centaines d’immeubles.
Dépendances
Crates externes:
uuid- Génération d’identifiants uniqueschrono- Gestion des timestamps UTCserde- Sérialisation JSONvalidator- Validation déclarative (email, longueur)
Modules internes:
Aucun (entité auto-suffisante)
Utilisation dans l’application
Création d’une organisation (use case):
// Enregistrement d'un nouveau syndic
let organization = Organization::new(
"Cabinet Syndic Paris 15".to_string(),
"contact@syndic-paris15.fr".to_string(),
Some("+33145678901".to_string()),
SubscriptionPlan::Starter,
)?;
// Sauvegarde via repository
organization_repository.create(organization).await?;
Vérification des limites avant ajout:
// Dans un use case d'ajout d'immeuble
let org = organization_repository.find_by_id(org_id).await?;
let building_count = building_repository.count_by_org(org_id).await?;
if !org.can_add_building(building_count) {
return Err(Error::SubscriptionLimitReached {
resource: "buildings",
current: building_count,
max: org.max_buildings,
});
}
// Créer l'immeuble...
Mise à niveau de plan:
// Quand l'utilisateur upgrade son abonnement
let mut org = organization_repository.find_by_id(org_id).await?;
org.upgrade_plan(SubscriptionPlan::Professional);
organization_repository.update(org).await?;
Notes de conception
Note
Slug unique:
Le slug est généré automatiquement depuis le nom mais n’est pas garanti unique. Pour un système de production, vous pourriez vouloir:
Ajouter une contrainte UNIQUE en base de données
Implémenter un système de suffixe (
company-name-2)Utiliser le slug pour des URL public-facing
Warning
Limites de ressources:
Les méthodes can_add_building() et can_add_user() vérifient uniquement les limites. Il est de la responsabilité du code appelant de:
Compter correctement les ressources actuelles
Appliquer ces vérifications avant création
Gérer les cas de courses (race conditions) en base
Tip
Soft delete recommandé:
Utilisez deactivate() plutôt que de supprimer les organisations pour:
Préserver l’intégrité référentielle (users, buildings liés)
Garder l’historique pour audit
Possibilité de réactivation avec données intactes
Fichiers associés
backend/src/domain/entities/user.rs- Entité User (liée via organization_id)backend/src/domain/entities/building.rs- Entité Building (liée via organization_id)backend/src/application/ports/organization_repository.rs- Trait repositorybackend/src/infrastructure/database/repositories/organization_repository_impl.rs- Implémentation PostgreSQLbackend/src/application/use_cases/organization_use_cases.rs- Cas d’usage (si existe)