Skip to main content

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}