Skip to main content

koprogo_api/application/dto/
convocation_dto.rs

1use crate::domain::entities::{Convocation, ConvocationStatus, ConvocationType};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct ConvocationResponse {
8    pub id: Uuid,
9    /// L'ACP propriétaire de la pièce (ADR-0045).
10    pub acp_id: Uuid,
11    pub organization_id: Uuid,
12    pub building_id: Uuid,
13    pub meeting_id: Uuid,
14    pub meeting_type: ConvocationType,
15    pub meeting_date: DateTime<Utc>,
16    pub status: ConvocationStatus,
17
18    // Legal deadline tracking
19    pub minimum_send_date: DateTime<Utc>,
20    pub actual_send_date: Option<DateTime<Utc>>,
21    pub scheduled_send_date: Option<DateTime<Utc>>,
22
23    // PDF generation
24    pub pdf_file_path: Option<String>,
25    pub language: String,
26
27    // Tracking
28    pub total_recipients: i32,
29    pub opened_count: i32,
30    pub will_attend_count: i32,
31    pub will_not_attend_count: i32,
32
33    // Computed fields
34    pub opening_rate: f64,             // opened / total * 100
35    pub attendance_rate: f64,          // will_attend / total * 100
36    pub days_until_meeting: i64,       // Computed from meeting_date
37    pub respects_legal_deadline: bool, // Computed: actual_send_date <= minimum_send_date
38
39    // Reminders
40    pub reminder_sent_at: Option<DateTime<Utc>>,
41
42    // 2e convocation — Art. 3.87 §5 CC
43    pub first_meeting_id: Option<Uuid>,
44    pub no_quorum_required: bool, // TRUE for second convocations (quorum not required per Art. 3.87 §5)
45
46    // Audit
47    pub created_at: DateTime<Utc>,
48    pub updated_at: DateTime<Utc>,
49    pub created_by: Uuid,
50}
51
52impl From<Convocation> for ConvocationResponse {
53    fn from(convocation: Convocation) -> Self {
54        let opening_rate = convocation.opening_rate();
55        let attendance_rate = convocation.attendance_rate();
56        let days_until_meeting = convocation.days_until_meeting();
57        let respects_legal_deadline = convocation.respects_legal_deadline();
58
59        Self {
60            id: convocation.id,
61            acp_id: convocation.acp_id,
62            organization_id: convocation.organization_id,
63            building_id: convocation.building_id,
64            meeting_id: convocation.meeting_id,
65            meeting_type: convocation.meeting_type,
66            meeting_date: convocation.meeting_date,
67            status: convocation.status,
68            minimum_send_date: convocation.minimum_send_date,
69            actual_send_date: convocation.actual_send_date,
70            scheduled_send_date: convocation.scheduled_send_date,
71            pdf_file_path: convocation.pdf_file_path,
72            language: convocation.language,
73            total_recipients: convocation.total_recipients,
74            opened_count: convocation.opened_count,
75            will_attend_count: convocation.will_attend_count,
76            will_not_attend_count: convocation.will_not_attend_count,
77            opening_rate,
78            attendance_rate,
79            days_until_meeting,
80            respects_legal_deadline,
81            reminder_sent_at: convocation.reminder_sent_at,
82            first_meeting_id: convocation.first_meeting_id,
83            no_quorum_required: convocation.no_quorum_required,
84            created_at: convocation.created_at,
85            updated_at: convocation.updated_at,
86            created_by: convocation.created_by,
87        }
88    }
89}
90
91#[derive(Debug, Deserialize)]
92pub struct CreateConvocationRequest {
93    pub building_id: Uuid,
94    pub meeting_id: Uuid,
95    pub meeting_type: ConvocationType,
96    pub meeting_date: DateTime<Utc>,
97    pub language: String, // FR, NL, DE, EN
98}
99
100#[derive(Debug, Deserialize)]
101pub struct ScheduleConvocationRequest {
102    pub send_date: DateTime<Utc>,
103}
104
105#[derive(Debug, Deserialize)]
106pub struct SendConvocationRequest {
107    /// Les destinataires, **facultatifs**.
108    ///
109    /// Absents, le serveur convoque tous les copropriétaires actifs de
110    /// l'immeuble. Ce champ était obligatoire et le frontend envoyait `{}` :
111    /// l'extracteur `web::Json` rejetait la requête en 400 avant même
112    /// d'entrer dans le gestionnaire, si bien que le bouton « Envoyer »
113    /// paraissait sans effet. Constaté en recette le 2026-09-06 (RN-10),
114    /// premier des trois verrous qui empêchent une AG d'aboutir (#780).
115    ///
116    /// `None` (champ absent, ancien client) : le serveur déduit tous les
117    /// copropriétaires actifs de l'immeuble. `Some(vec![])` (l'écran de
118    /// sélection rendu, tout décoché) : refusé explicitement, pas remplacé en
119    /// silence par le défaut — un renoncement délibéré n'est pas une absence
120    /// de choix. Voir `ConvocationUseCases::list_eligible_recipients`.
121    #[serde(default)]
122    pub recipient_owner_ids: Option<Vec<Uuid>>,
123}
124
125/// Un copropriétaire qu'une convocation toucherait, pour l'écran de
126/// sélection des destinataires (#780 verrou 1, #784).
127///
128/// Distinct de `ConvocationRecipientResponse` : celui-ci décrit un
129/// destinataire déjà rattaché à une convocation envoyée (tracking d'ouverture,
130/// de présence, de procuration). Celui-ci décrit un copropriétaire ÉLIGIBLE,
131/// avant tout envoi — la question posée est « qui pourrait recevoir ceci ? »,
132/// pas « qui l'a reçu ? ».
133#[derive(Debug, Serialize, Deserialize, Clone, utoipa::ToSchema)]
134pub struct EligibleRecipientResponse {
135    pub owner_id: Uuid,
136    pub full_name: String,
137    pub email: String,
138}
139
140#[derive(Debug, Serialize)]
141pub struct ConvocationSummaryResponse {
142    pub id: Uuid,
143    pub meeting_id: Uuid,
144    pub meeting_date: DateTime<Utc>,
145    pub status: ConvocationStatus,
146    pub total_recipients: i32,
147    pub opened_count: i32,
148    pub will_attend_count: i32,
149    pub days_until_meeting: i64,
150}
151
152impl From<Convocation> for ConvocationSummaryResponse {
153    fn from(convocation: Convocation) -> Self {
154        Self {
155            id: convocation.id,
156            meeting_id: convocation.meeting_id,
157            meeting_date: convocation.meeting_date,
158            status: convocation.status.clone(),
159            total_recipients: convocation.total_recipients,
160            opened_count: convocation.opened_count,
161            will_attend_count: convocation.will_attend_count,
162            days_until_meeting: convocation.days_until_meeting(),
163        }
164    }
165}