Skip to main content

koprogo_api/application/dto/
filters.rs

1use crate::domain::entities::ApprovalStatus;
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::Deserialize;
5use uuid::Uuid;
6
7/// Filters for building list queries
8#[derive(Debug, Deserialize, Default, Clone)]
9pub struct BuildingFilters {
10    /// Story 1.3 — Scope organisation : filtre les buildings dont
11    /// l'ACP parente appartient à cette organisation. Le repository
12    /// traduit en `acp_id IN (SELECT id FROM acps WHERE organization_id = $)`
13    /// (la colonne `buildings.organization_id` a été DROP en migration 040000).
14    pub organization_id: Option<Uuid>,
15    /// Story 1.2 — Scope ACP direct (filtre `buildings.acp_id = $`).
16    pub acp_id: Option<Uuid>,
17    pub city: Option<String>,
18    pub construction_year: Option<i32>,
19    pub min_units: Option<i32>,
20    pub max_units: Option<i32>,
21    /// BUG-WF14-2: Si défini, filtre les buildings où cet user possède un lot (via owners.user_id → unit_owners → units)
22    pub owner_user_id: Option<Uuid>,
23    /// Recherche libre (ILIKE) sur name/city/address — évite au frontend de
24    /// devoir fetch les 100 premiers buildings puis filtrer en mémoire
25    /// (BuildingSelector : ratait les buildings récents une fois >100
26    /// buildings créés globalement en CI, cf. searchBuildings côté frontend).
27    pub search: Option<String>,
28}
29
30/// Filters for expense list queries
31#[derive(Debug, Deserialize, Default, Clone)]
32pub struct ExpenseFilters {
33    pub organization_id: Option<Uuid>,
34    pub building_id: Option<Uuid>,
35    pub category: Option<String>,
36    pub status: Option<String>,
37    pub paid: Option<bool>,
38    pub approval_status: Option<ApprovalStatus>, // Nouveau: pour filtrer par statut workflow
39    pub date_from: Option<DateTime<Utc>>,
40    pub date_to: Option<DateTime<Utc>>,
41    pub min_amount: Option<Decimal>,
42    pub max_amount: Option<Decimal>,
43}
44
45/// Filters for unit list queries
46#[derive(Debug, Deserialize, Default, Clone)]
47pub struct UnitFilters {
48    /// Story H15 — Scope organisation : filtre les lots dont l'ACP appartient
49    /// à cette organisation. Le repository traduit en
50    /// `acp_id IN (SELECT id FROM acps WHERE organization_id = $)` (la colonne
51    /// `units.organization_id` a été DROP en migration 20260630030000).
52    pub organization_id: Option<Uuid>,
53    /// Story H15 — Scope ACP direct (filtre `units.acp_id = $`).
54    pub acp_id: Option<Uuid>,
55    pub building_id: Option<Uuid>,
56    pub unit_type: Option<String>,
57    pub has_owner: Option<bool>,
58    pub floor: Option<i32>,
59    pub min_area: Option<f64>,
60    pub max_area: Option<f64>,
61}
62
63/// Filters for owner list queries
64#[derive(Debug, Deserialize, Default, Clone)]
65pub struct OwnerFilters {
66    pub organization_id: Option<Uuid>,
67    pub email: Option<String>,
68    pub phone: Option<String>,
69    pub last_name: Option<String>,
70    pub first_name: Option<String>,
71}
72
73/// Filters for work report list queries
74#[derive(Debug, Deserialize, Default, Clone)]
75pub struct WorkReportFilters {
76    pub organization_id: Option<Uuid>,
77    pub building_id: Option<Uuid>,
78    pub work_type: Option<String>,
79    pub warranty_type: Option<String>,
80    pub contractor_name: Option<String>,
81    pub work_date_from: Option<DateTime<Utc>>,
82    pub work_date_to: Option<DateTime<Utc>>,
83    // ADR-0008 : bornes de coût en `Decimal`, comme la colonne
84    // `work_reports.cost` qu'elles filtrent.
85    //
86    // ATTENTION — ces deux bornes, comme `warranty_type`, `contractor_name`,
87    // `work_date_from`, `work_date_to` et `warranty_active`, sont acceptées
88    // par l'API mais **jamais appliquées** : `work_report_repository_impl`
89    // ne lit que `building_id` et `work_type`. Un appelant qui passe
90    // `?min_cost=1000` reçoit la liste NON filtrée en croyant l'inverse.
91    // Défaut constaté en convertissant ces champs — tracé, non corrigé ici
92    // (le corriger demande 7 filtres + tests 4-cat, hors périmètre ADR-0008).
93    pub min_cost: Option<Decimal>,
94    pub max_cost: Option<Decimal>,
95    pub warranty_active: Option<bool>,
96}
97
98/// Filters for technical inspection list queries
99#[derive(Debug, Deserialize, Default, Clone)]
100pub struct TechnicalInspectionFilters {
101    pub organization_id: Option<Uuid>,
102    pub building_id: Option<Uuid>,
103    pub inspection_type: Option<String>,
104    pub status: Option<String>,
105    pub inspector_name: Option<String>,
106    pub inspector_company: Option<String>,
107    pub inspection_date_from: Option<DateTime<Utc>>,
108    pub inspection_date_to: Option<DateTime<Utc>>,
109    pub overdue: Option<bool>,
110    pub compliant: Option<bool>,
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_building_filters_default() {
119        let filters = BuildingFilters::default();
120        assert!(filters.city.is_none());
121        assert!(filters.construction_year.is_none());
122        assert!(filters.min_units.is_none());
123        assert!(filters.max_units.is_none());
124    }
125
126    #[test]
127    fn test_expense_filters_default() {
128        let filters = ExpenseFilters::default();
129        assert!(filters.building_id.is_none());
130        assert!(filters.category.is_none());
131        assert!(filters.paid.is_none());
132    }
133
134    #[test]
135    fn test_unit_filters_default() {
136        let filters = UnitFilters::default();
137        assert!(filters.building_id.is_none());
138        assert!(filters.has_owner.is_none());
139    }
140
141    #[test]
142    fn test_owner_filters_default() {
143        let filters = OwnerFilters::default();
144        assert!(filters.email.is_none());
145        assert!(filters.last_name.is_none());
146    }
147}