koprogo_api/application/ports/acp_repository.rs
1//! Port (trait) pour le repository ACP — Story 1.1.
2//!
3//! Hexagonal : ce trait vit côté application, l'implémentation PostgreSQL vit
4//! dans `infrastructure/database/repositories/acp_repository_impl.rs`.
5//!
6//! Toutes les méthodes retournent `Result<_, AppError>` (CRITICAL.md §4 —
7//! pas de `Result<_, String>` pour les NEW use-cases).
8
9use crate::application::error::AppError;
10use crate::domain::entities::{Acp, AcpMetrics};
11use async_trait::async_trait;
12use uuid::Uuid;
13
14/// Scope de filtrage pour `list`.
15///
16/// - `All` : admin sans filtre (voit toutes les ACPs).
17/// - `Organization` : syndic / accountant — toutes les ACPs d'un cabinet.
18/// - `Owner` : owner / cdc — uniquement les ACPs où l'utilisateur a un
19/// `UserRoleAssignment` actif (scope=acp ou via building/unit).
20/// Story 1.3 enrichira ce scope (filtre transitif via building/unit) —
21/// ici on s'en tient au scope direct ACP.
22#[derive(Debug, Clone)]
23pub enum ListScope {
24 All,
25 Organization(Uuid),
26 Owner(Uuid),
27}
28
29/// Port repository ACP.
30#[async_trait]
31pub trait AcpRepository: Send + Sync {
32 /// Persiste une nouvelle ACP. Retourne l'entité telle que stockée
33 /// (incl. `created_at` / `updated_at` côté DB si différents).
34 async fn create(&self, acp: &Acp) -> Result<Acp, AppError>;
35
36 /// Récupère par id. `None` si absent (pas une erreur).
37 async fn find_by_id(&self, id: Uuid) -> Result<Option<Acp>, AppError>;
38
39 /// Story H6 (CL1) — récupère l'ACP **avec ses métriques agrégées** (Σ units,
40 /// Σ lots déclarés, Σ quotités, nb blocs) sur TOUS ses buildings. Source
41 /// de vérité de la conformité ACP-level (`Acp::assert_conformant`, ADR-0010).
42 /// `None` si l'ACP n'existe pas.
43 async fn find_by_id_with_metrics(
44 &self,
45 id: Uuid,
46 ) -> Result<Option<(Acp, AcpMetrics)>, AppError>;
47
48 /// Liste filtrée par scope. Tri implémentation : `created_at DESC`.
49 async fn list(&self, scope: ListScope) -> Result<Vec<Acp>, AppError>;
50
51 /// La même liste, **avec les métriques de chaque ACP**.
52 ///
53 /// ── Pourquoi une seconde méthode plutôt qu'un enrichissement ────────
54 ///
55 /// `find_by_id_with_metrics` existait déjà, mais par identifiant : afficher
56 /// une table de quatre ACP demandait quatre allers-retours, et cinquante
57 /// en demandaient cinquante. Le tableau de bord du syndic montre ses ACP
58 /// avec leurs blocs, leurs lots encodés et déclarés, et la somme de leurs
59 /// quotités — c'est une lecture de liste, pas quatre lectures unitaires.
60 ///
61 /// Elle est SÉPARÉE de `list` parce que les métriques coûtent quatre
62 /// sous-requêtes par ligne : les appelants qui n'ont besoin que des noms
63 /// — un sélecteur, une liste déroulante — ne doivent pas les payer.
64 ///
65 /// Les sous-requêtes sont **identiques** à celles de
66 /// `find_by_id_with_metrics`. Deux définitions divergentes de « lots
67 /// encodés » seraient pires que pas de table du tout : la fiche d'une ACP
68 /// et la ligne de la même ACP dans la liste afficheraient des nombres
69 /// différents, sans qu'on sache lequel croire.
70 async fn list_with_metrics(&self, scope: ListScope)
71 -> Result<Vec<(Acp, AcpMetrics)>, AppError>;
72
73 /// Met à jour une ACP existante (UPDATE … WHERE id = $1).
74 /// Retourne `AppError::NotFound` si aucune ligne affectée.
75 async fn update(&self, acp: &Acp) -> Result<Acp, AppError>;
76
77 /// "Archive" = DELETE physique pour cette Story 1.1 (pas de soft-delete
78 /// en v0.1.0 sur cette table ; la story 5.1 introduira `archived_at` sur
79 /// `acp_enabled_modules`, pas ici). Retourne `Ok(())` si suppression OK,
80 /// `AppError::NotFound` si aucune ligne affectée.
81 async fn archive(&self, id: Uuid) -> Result<(), AppError>;
82
83 /// Compte les buildings rattachés à l'ACP (utilisé par use-cases /
84 /// fiche ACP). Renvoie 0 tant que la story 1.2 n'a pas ajouté la
85 /// colonne `buildings.acp_id` ; alors la méthode reste compatible
86 /// puisque le SQL utilise `COALESCE`.
87 async fn count_buildings(&self, id: Uuid) -> Result<i64, AppError>;
88}