koprogo_api/application/dto/expense_dto.rs
1//! Expense DTOs — monetary fields use `rust_decimal::Decimal` (cf. ADR-0007).
2//!
3//! Note : `validator` crate `#[validate(range(...))]` ne support pas Decimal
4//! avec literals de type f64. Les invariants montants (> 0, taux VAT 0-100)
5//! sont enforced dans `Expense::new` / `Expense::new_with_vat` côté domaine
6//! (cf. `domain/entities/expense.rs`).
7
8use crate::domain::entities::{ApprovalStatus, ExpenseCategory, PaymentStatus};
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use validator::Validate;
12
13// ========== Legacy DTOs (backward compatibility) ==========
14
15#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
16#[serde(deny_unknown_fields)]
17pub struct CreateExpenseDto {
18 #[serde(default)]
19 pub organization_id: String,
20 pub building_id: String,
21 pub category: ExpenseCategory,
22
23 #[validate(length(min = 1))]
24 pub description: String,
25
26 /// Montant TTC (validé > 0 dans Expense::new). Decimal exact (cf. ADR-0007).
27 pub amount: Decimal,
28
29 pub expense_date: String,
30 pub supplier: Option<String>,
31 pub invoice_number: Option<String>,
32
33 /// Optional Belgian PCMN account code (e.g., "604001" for electricity)
34 /// Must reference an existing account in the organization's chart of accounts
35 #[validate(length(max = 40))]
36 pub account_code: Option<String>,
37
38 // ── Champs que le formulaire envoyait déjà, et que serde jetait ──────
39 //
40 // `InvoiceForm.svelte` poste sur `/expenses` en envoyant `amount_excl_vat`,
41 // `vat_rate` et `due_date`. Ces trois champs n'existaient que sur
42 // `CreateInvoiceDraftDto`, servi par `POST /invoices/draft` — une AUTRE
43 // route, que l'interface n'appelle pas.
44 //
45 // Conséquences mesurées, constats F12 et F20 du rapport du 2026-09-01 :
46 // la date d'échéance saisie s'affichait « - » sur la fiche dépense, et la
47 // décomposition HT/TVA n'était jamais renseignée, si bien que seul le TTC
48 // pouvait être affiché.
49 //
50 // Les accepter ici plutôt que de rebrancher le formulaire sur
51 // `/invoices/draft` : cette route-là crée une facture en statut `Draft`,
52 // à soumettre puis approuver. Changer d'endpoint changerait le workflow
53 // visible de l'utilisateur, ce qui est une décision de produit ; accepter
54 // les champs ne change que le fait de ne plus perdre la saisie.
55 /// Montant HT. Fourni avec `vat_rate`, la TVA est calculée et le TTC
56 /// déduit ; `amount` est alors ignoré au profit du calcul exact.
57 pub amount_excl_vat: Option<Decimal>,
58
59 /// Taux de TVA en POURCENTAGE (21.0 pour 21 %), validé 0..=100.
60 pub vat_rate: Option<Decimal>,
61
62 /// Échéance de règlement fournisseur (ISO 8601).
63 pub due_date: Option<String>,
64
65 /// Le détail de la facture, quand elle est saisie ligne par ligne.
66 ///
67 /// Absent en saisie simple : la dépense ne porte alors que ses totaux.
68 #[serde(default)]
69 pub line_items: Option<Vec<NouvelleLigneDeFactureDto>>,
70}
71
72#[derive(Debug, Serialize, utoipa::ToSchema)]
73pub struct ExpenseResponseDto {
74 pub id: String,
75 /// ACP propriétaire de la charge — clé de rattachement patrimonial.
76 /// Suit la copropriété lors des passations de syndic.
77 pub acp_id: String,
78 pub building_id: String,
79 pub category: ExpenseCategory,
80 pub description: String,
81 pub amount: Decimal,
82 pub expense_date: String,
83 pub payment_status: PaymentStatus,
84 pub approval_status: ApprovalStatus,
85 pub supplier: Option<String>,
86 pub invoice_number: Option<String>,
87 /// Belgian PCMN account code if linked to chart of accounts
88 pub account_code: Option<String>,
89 /// Contractor report reference for Works category (Issue #309)
90 pub contractor_report_id: Option<String>,
91
92 // ── Champs présents en base, absents de la réponse ──────────────────
93 //
94 // `expenses` porte `due_date`, `amount_excl_vat`, `vat_rate`,
95 // `vat_amount` et `amount_incl_vat` ; cette réponse n'en renvoyait aucun.
96 // La fiche dépense affichait donc « - » pour l'échéance (constat F12) et
97 // ne pouvait montrer que le TTC (constat F20) — non parce que la donnée
98 // manquait, mais parce que le contrat de sortie l'omettait.
99 //
100 // Trois couches manquaient les mêmes champs : le DTO d'entrée les jetait,
101 // le constructeur ne les posait pas, et cette réponse ne les exposait pas.
102 /// Échéance de règlement fournisseur (ISO 8601).
103 pub due_date: Option<String>,
104 /// Montant hors TVA.
105 pub amount_excl_vat: Option<Decimal>,
106 /// Taux de TVA en POURCENTAGE (21.0 pour 21 %).
107 pub vat_rate: Option<Decimal>,
108 /// Montant de TVA.
109 pub vat_amount: Option<Decimal>,
110 /// Montant TVA comprise.
111 pub amount_incl_vat: Option<Decimal>,
112}
113
114// ========== New Invoice DTOs (with VAT & Workflow) ==========
115
116/// Créer une facture brouillon avec gestion TVA.
117/// Validation des montants > 0 et taux 0-100 effectuée dans `Expense::new_with_vat`.
118#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
119#[serde(deny_unknown_fields)]
120pub struct CreateInvoiceDraftDto {
121 #[serde(default)]
122 pub organization_id: String,
123 pub building_id: String,
124 pub category: ExpenseCategory,
125
126 #[validate(length(min = 1))]
127 pub description: String,
128
129 /// Montant HT (validé > 0 dans `Expense::new_with_vat`).
130 pub amount_excl_vat: Decimal,
131
132 /// Taux TVA en % (validé 0..=100 dans `Expense::new_with_vat`).
133 pub vat_rate: Decimal,
134
135 pub invoice_date: String, // ISO 8601
136 pub due_date: Option<String>, // ISO 8601
137 pub supplier: Option<String>,
138 pub invoice_number: Option<String>,
139}
140
141/// Modifier une facture brouillon ou rejetée.
142#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
143#[serde(deny_unknown_fields)]
144pub struct UpdateInvoiceDraftDto {
145 #[validate(length(min = 1))]
146 pub description: Option<String>,
147
148 pub category: Option<ExpenseCategory>,
149
150 pub amount_excl_vat: Option<Decimal>,
151 pub vat_rate: Option<Decimal>,
152
153 pub invoice_date: Option<String>,
154 pub due_date: Option<String>,
155 pub supplier: Option<String>,
156 pub invoice_number: Option<String>,
157}
158
159/// Soumettre une facture pour validation (Draft → PendingApproval).
160#[derive(Debug, Deserialize, Clone, utoipa::ToSchema)]
161pub struct SubmitForApprovalDto {
162 // Empty body, action via PUT /invoices/:id/submit
163}
164
165/// Approuver une facture (PendingApproval → Approved).
166#[derive(Debug, Deserialize, Clone, utoipa::ToSchema)]
167pub struct ApproveInvoiceDto {
168 pub approved_by_user_id: String, // User ID du syndic/admin
169}
170
171/// Rejeter une facture avec raison (PendingApproval → Rejected).
172#[derive(Debug, Deserialize, Validate, Clone, utoipa::ToSchema)]
173pub struct RejectInvoiceDto {
174 pub rejected_by_user_id: String,
175
176 #[validate(length(min = 1))]
177 pub rejection_reason: String,
178}
179
180/// Une ligne de facture transmise **à la création** de la dépense.
181///
182/// Distincte de `CreateInvoiceLineItemDto`, qui exige un `expense_id` parce
183/// qu'elle sert à ajouter une ligne à une facture déjà enregistrée. À la
184/// création, la dépense n'a pas encore d'identifiant : le lien se fait après
185/// coup, côté use-case.
186///
187/// Sans ce type, `InvoiceForm.svelte` envoyait `line_items` dans le corps et
188/// serde les jetait en silence : la facture était créée avec ses totaux, et
189/// le détail — description, quantité, prix unitaire, TVA de chaque ligne —
190/// disparaissait sans le moindre avertissement. Un comptable saisissant une
191/// facture ligne par ligne perdait son travail. Constaté le 2026-09-04.
192#[derive(Debug, Clone, Deserialize, Serialize, Validate, utoipa::ToSchema)]
193#[serde(deny_unknown_fields)]
194pub struct NouvelleLigneDeFactureDto {
195 #[validate(length(min = 1))]
196 pub description: String,
197 pub quantity: Decimal,
198 pub unit_price: Decimal,
199 pub vat_rate: Decimal,
200}
201
202/// Créer une ligne de facture.
203/// Validations (quantity > 0, unit_price ≥ 0, vat_rate 0..=100) dans `InvoiceLineItem::new`.
204#[derive(Debug, Deserialize, Validate, Clone)]
205#[serde(deny_unknown_fields)]
206pub struct CreateInvoiceLineItemDto {
207 pub expense_id: String,
208
209 #[validate(length(min = 1))]
210 pub description: String,
211
212 pub quantity: Decimal,
213 pub unit_price: Decimal,
214 pub vat_rate: Decimal,
215}
216
217// ========== Response DTOs ==========
218
219/// Response enrichie avec tous les champs invoice/workflow.
220#[derive(Debug, Serialize, Clone)]
221pub struct InvoiceResponseDto {
222 pub id: String,
223 pub organization_id: String,
224 pub building_id: String,
225 pub category: ExpenseCategory,
226 pub description: String,
227
228 // Montants — exact decimal (cf. ADR-0007)
229 pub amount: Decimal, // TTC (backward compatibility)
230 pub amount_excl_vat: Option<Decimal>,
231 pub vat_rate: Option<Decimal>,
232 pub vat_amount: Option<Decimal>,
233 pub amount_incl_vat: Option<Decimal>,
234
235 // Dates
236 pub expense_date: String,
237 pub invoice_date: Option<String>,
238 pub due_date: Option<String>,
239 pub paid_date: Option<String>,
240
241 // Workflow
242 pub approval_status: ApprovalStatus,
243 pub submitted_at: Option<String>,
244 pub approved_by: Option<String>,
245 pub approved_at: Option<String>,
246 pub rejection_reason: Option<String>,
247
248 // Payment
249 pub payment_status: PaymentStatus,
250 pub supplier: Option<String>,
251 pub invoice_number: Option<String>,
252
253 /// Contractor report reference for Works category (Issue #309)
254 pub contractor_report_id: Option<String>,
255
256 pub created_at: String,
257 pub updated_at: String,
258}
259
260/// Response pour une ligne de facture.
261#[derive(Debug, Serialize)]
262pub struct InvoiceLineItemResponseDto {
263 pub id: String,
264 pub expense_id: String,
265 pub description: String,
266 pub quantity: Decimal,
267 pub unit_price: Decimal,
268 pub amount_excl_vat: Decimal,
269 pub vat_rate: Decimal,
270 pub vat_amount: Decimal,
271 pub amount_incl_vat: Decimal,
272 pub created_at: String,
273}
274
275/// Response pour une répartition de charge.
276#[derive(Debug, Serialize)]
277pub struct ChargeDistributionResponseDto {
278 pub id: String,
279 pub expense_id: String,
280 pub unit_id: String,
281 pub owner_id: String,
282 /// Quote-part (e.g., dec!(0.25) pour 25%). Decimal exact (cf. ADR-0007).
283 pub quota_percentage: Decimal,
284 pub amount_due: Decimal,
285 /// Story H12 — critère légal de répartition (value / utility / mixed).
286 pub distribution_criteria: String,
287 pub created_at: String,
288}
289
290/// Liste des factures en attente d'approbation (pour syndics).
291#[derive(Debug, Serialize)]
292pub struct PendingInvoicesListDto {
293 pub invoices: Vec<InvoiceResponseDto>,
294 pub count: usize,
295}