koprogo_api/application/ports/portfolio_repository.rs
1//! Port (trait) pour le repository Portfolio — Story 2.1.
2//!
3//! Hexagonal : trait côté application, implémentation PostgreSQL dans
4//! `infrastructure/database/repositories/portfolio_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//!
9//! Source : `docs/maury/refonte-ux-multi-role-acp/architecture.md` §3.1.
10
11use crate::application::error::AppError;
12use crate::domain::entities::{Portfolio, PortfolioBuilding, PortfolioShare};
13use async_trait::async_trait;
14use uuid::Uuid;
15
16/// Entrée de listing : un building du portfolio + flag favori + ordre
17/// déterministe (favoris d'abord, puis `added_at DESC`).
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PortfolioBuildingEntry {
20 pub portfolio_id: Uuid,
21 pub building_id: Uuid,
22 pub is_favorite: bool,
23}
24
25/// Port repository Portfolio.
26#[async_trait]
27pub trait PortfolioRepository: Send + Sync {
28 /// Persiste un nouveau portfolio. Retourne l'entité telle que stockée.
29 async fn create(&self, portfolio: &Portfolio) -> Result<Portfolio, AppError>;
30
31 /// Récupère par id. `None` si absent (pas une erreur).
32 async fn find_by_id(&self, id: Uuid) -> Result<Option<Portfolio>, AppError>;
33
34 /// Liste les portfolios dont `user_id` est `owner_user_id` ou
35 /// figure dans `portfolio_shares`. Tri : `created_at DESC`.
36 async fn list_for_user(&self, user_id: Uuid) -> Result<Vec<Portfolio>, AppError>;
37
38 /// Met à jour un portfolio existant. Retourne `AppError::NotFound`
39 /// si aucune ligne affectée.
40 async fn update(&self, portfolio: &Portfolio) -> Result<Portfolio, AppError>;
41
42 /// Supprime un portfolio (DELETE physique — cascade sur
43 /// `portfolio_buildings` et `portfolio_shares`).
44 async fn delete(&self, id: Uuid) -> Result<(), AppError>;
45
46 /// Ajoute (ou remplace) un building dans le portfolio.
47 /// Idempotent : un `ON CONFLICT (portfolio_id, building_id) DO UPDATE`
48 /// rafraîchit `is_favorite` si la ligne existe déjà.
49 async fn add_building(
50 &self,
51 portfolio_id: Uuid,
52 building_id: Uuid,
53 is_favorite: bool,
54 ) -> Result<PortfolioBuilding, AppError>;
55
56 /// Retire un building du portfolio. `AppError::NotFound` si aucune
57 /// ligne supprimée.
58 async fn remove_building(&self, portfolio_id: Uuid, building_id: Uuid) -> Result<(), AppError>;
59
60 /// Liste les buildings d'un portfolio.
61 /// **Tri stable** : favoris d'abord (`is_favorite DESC`) puis
62 /// `added_at DESC` (cf. AC @happy Story 2.1).
63 async fn list_buildings(
64 &self,
65 portfolio_id: Uuid,
66 ) -> Result<Vec<PortfolioBuildingEntry>, AppError>;
67
68 /// Partage le portfolio avec un autre user.
69 /// Idempotent : `ON CONFLICT (portfolio_id, shared_with_user_id) DO UPDATE`
70 /// rafraîchit `can_edit`.
71 async fn share_with(
72 &self,
73 portfolio_id: Uuid,
74 shared_with_user_id: Uuid,
75 can_edit: bool,
76 ) -> Result<PortfolioShare, AppError>;
77
78 /// Retire un partage. `AppError::NotFound` si aucune ligne supprimée.
79 async fn unshare(&self, portfolio_id: Uuid, shared_with_user_id: Uuid) -> Result<(), AppError>;
80
81 /// Liste les partages d'un portfolio.
82 async fn list_shares(&self, portfolio_id: Uuid) -> Result<Vec<PortfolioShare>, AppError>;
83}