Skip to main content

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}