Skip to main content

koprogo_api/application/dto/
unit_dto.rs

1use crate::domain::entities::UnitType;
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4use validator::Validate;
5
6#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
7#[serde(deny_unknown_fields)]
8pub struct CreateUnitDto {
9    /// Story H15 — FK vers `acps.id` (anciennement `organization_id`).
10    /// Le lot dérive son ACP de son building parent (cf. #602) ; le scoping
11    /// org se fait via `acps.organization_id`.
12    ///
13    /// OPTIONNEL depuis 2026-08-27. Le champ était obligatoire, ce qui
14    /// contredisait la ligne au-dessus : si l'ACP se dérive du building, le
15    /// client n'a pas à la fournir. Deux conséquences mesurées :
16    ///
17    ///   1. Un `POST /units` sans `acp_id` était rejeté par serde AVANT
18    ///      d'atteindre le handler, avec un corps en TEXTE BRUT
19    ///      (« Json deserialize error: missing field `acp_id` »). Le garde-fou
20    ///      `if dto.acp_id.is_empty()` du handler, qui rend un JSON propre,
21    ///      était donc mort pour ce cas : il ne se déclenchait que sur une
22    ///      chaîne vide explicite.
23    ///
24    ///   2. Tout appelant faisant `.json()` sur cette réponse recevait
25    ///      « Unexpected token 'J' », un message qui ne dit rien du défaut.
26    ///      C'est ce qui faisait échouer `02-ag-full-cycle` (gate de
27    ///      caractérisation) et taire `seedConformantUnits` en `status=400`.
28    ///
29    /// Absent ou vide, l'ACP est désormais lue sur le building parent, qui
30    /// est la source de vérité. Fournie, elle est utilisée telle quelle :
31    /// le comportement des appelants existants est inchangé.
32    #[serde(default)]
33    pub acp_id: Option<String>,
34    pub building_id: String,
35
36    #[validate(length(min = 1))]
37    pub unit_number: String,
38
39    pub unit_type: UnitType,
40    pub floor: Option<i32>,
41
42    #[validate(range(min = 0.1))]
43    pub surface_area: f64,
44
45    /// Quote-part en millièmes (Decimal exact, range 0.1..=1000 enforced en domain).
46    pub quota: Decimal,
47}
48
49/// `deny_unknown_fields` : un `PUT /units/{id}` portant `owner_id` repondait
50/// 200 en jetant le champ, laissant croire que le lot venait d'etre rattache a
51/// un proprietaire. `units.owner_id` est DEPRECIE depuis la migration
52/// `20250127000000_refactor_owners_multitenancy` — la relation vit dans
53/// `unit_owners` (API `/unit-owners`), qui porte les quotites et les dates.
54/// Le refus explicite renvoie desormais vers la bonne route au lieu de perdre
55/// la donnee en silence.
56#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
57#[serde(deny_unknown_fields)]
58pub struct UpdateUnitDto {
59    #[validate(length(min = 1))]
60    pub unit_number: String,
61
62    pub unit_type: UnitType,
63    pub floor: i32,
64
65    #[validate(range(min = 0.1))]
66    pub surface_area: f64,
67
68    /// Quote-part en millièmes (Decimal exact, range 0.1..=1000 enforced en domain).
69    pub quota: Decimal,
70}
71
72#[derive(Debug, Serialize, utoipa::ToSchema)]
73pub struct UnitResponseDto {
74    pub id: String,
75    pub building_id: String,
76    pub unit_number: String,
77    pub unit_type: UnitType,
78    pub floor: Option<i32>,
79    pub surface_area: f64,
80    pub quota: Decimal,
81    pub owner_id: Option<String>,
82}