Skip to main content

koprogo_api/application/dto/
resolution_dto.rs

1use crate::domain::entities::{MajorityType, Resolution, ResolutionStatus, ResolutionType};
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Response DTO for Resolution
8#[derive(Debug, Serialize, Deserialize, Clone, utoipa::ToSchema)]
9pub struct ResolutionResponse {
10    pub id: Uuid,
11    pub meeting_id: Uuid,
12    pub title: String,
13    pub description: String,
14    pub resolution_type: ResolutionType,
15    pub majority_required: MajorityType,
16    pub vote_count_pour: i32,
17    pub vote_count_contre: i32,
18    pub vote_count_abstention: i32,
19    pub total_voting_power_pour: Decimal,
20    pub total_voting_power_contre: Decimal,
21    pub total_voting_power_abstention: Decimal,
22    pub status: ResolutionStatus,
23    pub agenda_item_index: Option<usize>, // Issue #310: Link to agenda item
24    pub created_at: DateTime<Utc>,
25    pub voted_at: Option<DateTime<Utc>>,
26    /// Story 4.6 (#581) — dérivé de `Resolution::is_auto_generated()`. Le
27    /// frontend s'en sert pour afficher `resolution-auto-badge` et masquer
28    /// la suppression/l'édition, en plus du refus 403 côté serveur.
29    pub is_auto_generated: bool,
30    // Calculated fields
31    pub total_votes: i32,
32    pub pour_percentage: f64,
33    pub contre_percentage: f64,
34    pub abstention_percentage: f64,
35}
36
37impl From<Resolution> for ResolutionResponse {
38    fn from(resolution: Resolution) -> Self {
39        Self {
40            id: resolution.id,
41            meeting_id: resolution.meeting_id,
42            title: resolution.title.clone(),
43            description: resolution.description.clone(),
44            resolution_type: resolution.resolution_type.clone(),
45            majority_required: resolution.majority_required.clone(),
46            vote_count_pour: resolution.vote_count_pour,
47            vote_count_contre: resolution.vote_count_contre,
48            vote_count_abstention: resolution.vote_count_abstention,
49            total_voting_power_pour: resolution.total_voting_power_pour,
50            total_voting_power_contre: resolution.total_voting_power_contre,
51            total_voting_power_abstention: resolution.total_voting_power_abstention,
52            status: resolution.status.clone(),
53            agenda_item_index: resolution.agenda_item_index,
54            created_at: resolution.created_at,
55            voted_at: resolution.voted_at,
56            is_auto_generated: resolution.is_auto_generated(),
57            // Calculated
58            total_votes: resolution.total_votes(),
59            pour_percentage: resolution.pour_percentage(),
60            contre_percentage: resolution.contre_percentage(),
61            abstention_percentage: resolution.abstention_percentage(),
62        }
63    }
64}
65
66/// Request DTO for creating a resolution
67#[derive(Debug, Deserialize, utoipa::ToSchema)]
68pub struct CreateResolutionRequest {
69    pub meeting_id: Uuid,
70    pub title: String,
71    pub description: String,
72    pub resolution_type: ResolutionType,
73    pub majority_required: MajorityType,
74    pub agenda_item_index: Option<usize>, // Issue #310: Optional link to agenda item
75}
76
77/// Request DTO for updating a resolution
78#[derive(Debug, Deserialize, utoipa::ToSchema)]
79pub struct UpdateResolutionRequest {
80    pub title: Option<String>,
81    pub description: Option<String>,
82    pub resolution_type: Option<ResolutionType>,
83    pub majority_required: Option<MajorityType>,
84}
85
86/// Request DTO for closing voting on a resolution
87#[derive(Debug, Deserialize, utoipa::ToSchema)]
88pub struct CloseVotingRequest {
89    /// Total des tantièmes de l'immeuble, **facultatif et ignoré**.
90    ///
91    /// Ce champ était obligatoire, et le frontend envoyait `{}` : la requête
92    /// échouait donc à la désérialisation, en 400, avant même d'atteindre le
93    /// gestionnaire. Le bouton « Clôturer le vote » paraissait inerte, ce qui
94    /// a été rapporté trois fois en recette (R3-3, RN-8) et bloquait la
95    /// deuxième des trois conditions de clôture d'une AG.
96    ///
97    /// Il est conservé pour ne pas casser les appelants existants, mais le
98    /// serveur ne s'en sert plus : le dénominateur de la majorité est
99    /// désormais lu sur l'immeuble. Le laisser fournir par le client
100    /// permettait de faire proclamer une majorité qui n'existe pas — signalé
101    /// dans l'issue #767.
102    #[serde(default)]
103    pub total_voting_power: Option<Decimal>,
104}