Skip to main content

koprogo_api/application/dto/
portfolio_dto.rs

1//! Portfolio DTOs — Story 2.1.
2//!
3//! Request/Response DTOs pour les endpoints `/portfolios`. Validation via
4//! `validator`. Bornes alignées avec les invariants `Portfolio::new`
5//! (cf. `domain/entities/portfolio.rs`).
6
7use serde::{Deserialize, Serialize};
8use validator::Validate;
9
10/// Création d'un portfolio.
11///
12/// `owner_user_id` est inféré côté handler depuis `AuthenticatedUser`
13/// — pas exposé dans le body pour éviter toute escalade.
14#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
15pub struct CreatePortfolioDto {
16    #[validate(length(
17        min = 2,
18        max = 120,
19        message = "Name must be between 2 and 120 characters"
20    ))]
21    pub name: String,
22
23    #[validate(length(max = 1000, message = "Description must be at most 1000 characters"))]
24    pub description: Option<String>,
25}
26
27/// Mise à jour d'un portfolio (PUT — état complet).
28#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
29pub struct UpdatePortfolioDto {
30    #[validate(length(
31        min = 2,
32        max = 120,
33        message = "Name must be between 2 and 120 characters"
34    ))]
35    pub name: String,
36
37    #[validate(length(max = 1000, message = "Description must be at most 1000 characters"))]
38    pub description: Option<String>,
39}
40
41/// Ajout d'un building au portfolio.
42#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
43pub struct AddBuildingDto {
44    pub building_id: String,
45    #[serde(default)]
46    pub is_favorite: bool,
47}
48
49/// Partage du portfolio avec un autre utilisateur.
50#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
51pub struct SharePortfolioDto {
52    pub shared_with_user_id: String,
53    #[serde(default)]
54    pub can_edit: bool,
55}
56
57/// Réponse JSON pour un portfolio.
58#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
59pub struct PortfolioResponseDto {
60    pub id: String,
61    pub owner_user_id: String,
62    pub name: String,
63    pub description: Option<String>,
64    pub created_at: String,
65    pub updated_at: String,
66}
67
68/// Building du portfolio (élément du listing trié favoris d'abord).
69#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
70pub struct PortfolioBuildingResponseDto {
71    pub portfolio_id: String,
72    pub building_id: String,
73    pub is_favorite: bool,
74}
75
76/// Partage du portfolio.
77#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
78pub struct PortfolioShareResponseDto {
79    pub portfolio_id: String,
80    pub shared_with_user_id: String,
81    pub can_edit: bool,
82    pub shared_at: String,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn happy_valid_create_dto_passes_validation() {
91        let dto = CreatePortfolioDto {
92            name: "Mes favoris".to_string(),
93            description: None,
94        };
95        assert!(dto.validate().is_ok());
96    }
97
98    #[test]
99    fn negative_too_short_name_fails_validation() {
100        let dto = CreatePortfolioDto {
101            name: "A".to_string(),
102            description: None,
103        };
104        assert!(dto.validate().is_err());
105    }
106
107    #[test]
108    fn negative_too_long_description_fails_validation() {
109        let dto = CreatePortfolioDto {
110            name: "Name".to_string(),
111            description: Some("x".repeat(1001)),
112        };
113        assert!(dto.validate().is_err());
114    }
115
116    #[test]
117    fn negative_empty_name_fails_validation() {
118        let dto = CreatePortfolioDto {
119            name: "".to_string(),
120            description: None,
121        };
122        assert!(dto.validate().is_err());
123    }
124
125    #[test]
126    fn happy_share_dto_default_can_edit_false() {
127        let json = r#"{"shared_with_user_id": "00000000-0000-0000-0000-000000000001"}"#;
128        let dto: SharePortfolioDto = serde_json::from_str(json).unwrap();
129        assert!(!dto.can_edit);
130    }
131}