Skip to main content

koprogo_api/application/dto/
meeting_dto.rs

1use crate::domain::entities::{Meeting, MeetingStatus, MeetingType};
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Response DTO for Meeting
8#[derive(Debug, Serialize, Deserialize)]
9pub struct MeetingResponse {
10    pub id: Uuid,
11    /// L'ACP dont c'est l'assemblée (Art. 3.87 § 1er, ADR-0045).
12    ///
13    /// La réponse n'expose délibérément pas le syndic : ce qu'un client a
14    /// besoin de savoir, c'est de quelle copropriété relève l'assemblée, pas
15    /// qui la tenait au moment où elle s'est réunie.
16    pub acp_id: Uuid,
17    pub building_id: Uuid,
18    pub meeting_type: MeetingType,
19    pub title: String,
20    pub description: Option<String>,
21    pub scheduled_date: DateTime<Utc>,
22    pub location: String,
23    pub status: MeetingStatus,
24    pub agenda: Vec<String>,
25    pub attendees_count: Option<i32>,
26    // Quorum — Art. 3.87 §5 CC
27    pub quorum_validated: bool,
28    pub quorum_percentage: Option<f64>,
29    pub total_quotas: Option<Decimal>,
30    pub present_quotas: Option<Decimal>,
31    pub is_second_convocation: bool,
32    pub minutes_document_id: Option<Uuid>,
33    pub minutes_sent_at: Option<DateTime<Utc>>,
34
35    // ── Délai de convocation, Art. 3.87 § 3 ────────────────────────────────
36    //
37    // Servis pour que l'écran puisse AVERTIR à la saisie, au lieu de laisser
38    // découvrir l'impasse au moment de convoquer.
39    //
40    // Recette du 2026-09-06 (RN-9) : une assemblée créée à cinq jours ne peut
41    // plus être convoquée régulièrement, et le bouton « Reporter » ne sortait
42    // de rien. Le syndic n'en sortait qu'en supprimant l'assemblée. La règle
43    // était juste, sa temporalité ne l'était pas (#780, verrou 1).
44    /// La date limite d'envoi d'une convocation régulière : quinze jours avant
45    /// l'assemblée. `None` si l'assemblée est déjà tenue.
46    pub date_limite_envoi_convocation: Option<DateTime<Utc>>,
47    /// Le délai de quinze jours peut-il encore être tenu ?
48    ///
49    /// `false` **n'interdit rien** : l'urgence est prévue par le texte
50    /// lui-même, une assemblée peut être encodée après coup, et une seconde
51    /// convocation subit la date de l'échec précédent.
52    pub convocation_encore_possible: bool,
53    /// De combien de jours faudrait-il reculer l'assemblée pour tenir le
54    /// délai. `None` quand il est tenu, ou que l'assemblée est passée.
55    pub jours_manquants_convocation: Option<i64>,
56
57    pub created_at: DateTime<Utc>,
58    pub updated_at: DateTime<Utc>,
59}
60
61impl From<Meeting> for MeetingResponse {
62    fn from(meeting: Meeting) -> Self {
63        // Le délai de convocation se DÉDUIT de la date et du type ; il ne se
64        // stocke pas. Un champ persisté serait faux dès le lendemain.
65        //
66        // La seconde convocation partage le même délai depuis la loi de 2019
67        // (Art. 3.87 § 5) : `is_second_convocation` ne change donc pas le
68        // calcul, et le rappeler ici évite qu'on croie l'avoir oublié.
69        let type_de_convocation = match meeting.meeting_type {
70            crate::domain::entities::MeetingType::Ordinary => {
71                crate::domain::copropriete::convocation::ConvocationType::Ordinary
72            }
73            crate::domain::entities::MeetingType::Extraordinary => {
74                crate::domain::copropriete::convocation::ConvocationType::Extraordinary
75            }
76        };
77        let delai = crate::domain::copropriete::delai_de_convocation::evaluer(
78            meeting.scheduled_date,
79            &type_de_convocation,
80            Utc::now(),
81        );
82        use crate::domain::copropriete::delai_de_convocation::DelaiDeConvocation;
83        let (
84            date_limite_envoi_convocation,
85            convocation_encore_possible,
86            jours_manquants_convocation,
87        ) = match delai {
88            DelaiDeConvocation::Tenable { date_limite_envoi } => {
89                (Some(date_limite_envoi), true, None)
90            }
91            DelaiDeConvocation::TropCourt {
92                date_limite_envoi,
93                jours_manquants,
94            } => (Some(date_limite_envoi), false, Some(jours_manquants)),
95            DelaiDeConvocation::DejaTenue => (None, false, None),
96        };
97
98        Self {
99            id: meeting.id,
100            acp_id: meeting.acp_id,
101            building_id: meeting.building_id,
102            meeting_type: meeting.meeting_type,
103            title: meeting.title,
104            description: meeting.description,
105            scheduled_date: meeting.scheduled_date,
106            location: meeting.location,
107            status: meeting.status,
108            agenda: meeting.agenda,
109            attendees_count: meeting.attendees_count,
110            quorum_validated: meeting.quorum_validated,
111            quorum_percentage: meeting.quorum_percentage,
112            total_quotas: meeting.total_quotas,
113            present_quotas: meeting.present_quotas,
114            is_second_convocation: meeting.is_second_convocation,
115            minutes_document_id: meeting.minutes_document_id,
116            minutes_sent_at: meeting.minutes_sent_at,
117            date_limite_envoi_convocation,
118            convocation_encore_possible,
119            jours_manquants_convocation,
120            created_at: meeting.created_at,
121            updated_at: meeting.updated_at,
122        }
123    }
124}
125
126/// Request DTO for validating quorum (Art. 3.87 §5 CC)
127#[derive(Debug, Deserialize)]
128pub struct ValidateQuorumRequest {
129    /// Millièmes présents + représentés par procuration (Decimal exact — ADR-0008)
130    pub present_quotas: Decimal,
131    /// Total millièmes du bâtiment (généralement 1000, Decimal exact — ADR-0008)
132    pub total_quotas: Decimal,
133}
134
135/// Request DTO for creating a meeting
136#[derive(Debug, Deserialize)]
137pub struct CreateMeetingRequest {
138    #[serde(default)]
139    pub organization_id: Uuid, // Will be overridden by JWT token
140    pub building_id: Uuid,
141    pub meeting_type: MeetingType,
142    pub title: String,
143    pub description: Option<String>,
144    pub scheduled_date: DateTime<Utc>,
145    pub location: String,
146    /// Art. 3.87 §5 CC: 2nd convocation skips quorum requirement
147    #[serde(default)]
148    pub is_second_convocation: bool,
149}
150
151/// Request DTO for updating a meeting
152#[derive(Debug, Deserialize)]
153pub struct UpdateMeetingRequest {
154    pub title: Option<String>,
155    pub description: Option<String>,
156    pub scheduled_date: Option<DateTime<Utc>>,
157    pub location: Option<String>,
158}
159
160/// Request DTO for adding agenda item
161#[derive(Debug, Deserialize)]
162pub struct AddAgendaItemRequest {
163    pub item: String,
164}
165
166/// Request DTO for completing a meeting
167#[derive(Debug, Deserialize)]
168pub struct CompleteMeetingRequest {
169    pub attendees_count: i32,
170}
171
172/// Request DTO for rescheduling a meeting
173#[derive(Debug, Deserialize)]
174pub struct RescheduleMeetingRequest {
175    pub scheduled_date: DateTime<Utc>,
176}
177
178/// Request DTO for attaching minutes document to a completed meeting (Issue #313)
179#[derive(Debug, Deserialize)]
180pub struct AttachMinutesRequest {
181    pub document_id: Uuid,
182}
183
184/// Request DTO for scheduling a second convocation (Issue #311)
185#[derive(Debug, Deserialize)]
186pub struct ScheduleSecondConvocationRequest {
187    pub building_id: Uuid,
188    pub first_meeting_id: Uuid,
189    pub new_meeting_date: DateTime<Utc>,
190    pub language: String,
191}