Skip to main content

koprogo_api/application/dto/
payment_reminder_dto.rs

1use crate::domain::entities::{DeliveryMethod, PaymentReminder, ReminderLevel, ReminderStatus};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4use validator::Validate;
5
6/// DTO for creating a new payment reminder
7#[derive(Debug, Deserialize, Validate, Clone)]
8pub struct CreatePaymentReminderDto {
9    pub organization_id: String,
10    pub expense_id: String,
11    pub owner_id: String,
12    pub level: ReminderLevel,
13
14    /// Montant dû en euros. `Decimal` exact (ADR-0007/0008, suite #661).
15    ///
16    /// Plus d'annotation `#[validate(range)]` : `validator` ne sait pas borner
17    /// un `Decimal`. L'invariant n'est pas perdu pour autant — il est porté par
18    /// `PaymentReminder::new`, qui rejette tout montant inférieur au centime.
19    /// C'est aussi la place correcte au regard de l'architecture hexagonale, et
20    /// le pattern déjà suivi par `expense_dto` / `budget_dto` pour leurs
21    /// montants.
22    #[serde(with = "rust_decimal::serde::float")]
23    pub amount_owed: Decimal,
24
25    pub due_date: String, // ISO 8601 format
26
27    #[validate(range(min = 0))]
28    pub days_overdue: i64,
29}
30
31/// DTO for payment reminder response
32#[derive(Debug, Serialize, Clone)]
33pub struct PaymentReminderResponseDto {
34    pub id: String,
35    /// L'ACP créancière de la somme réclamée (Art. 3.86 § 3, ADR-0045).
36    pub acp_id: String,
37    /// Le syndic qui relance. Trace d'auteur, pas un droit d'accès.
38    pub organization_id: String,
39    pub expense_id: String,
40    pub owner_id: String,
41    pub owner_name: Option<String>,  // Full name of owner for display
42    pub owner_email: Option<String>, // Owner email for contact
43    pub level: ReminderLevel,
44    pub status: ReminderStatus,
45    #[serde(with = "rust_decimal::serde::float")]
46    pub amount_owed: Decimal,
47    #[serde(with = "rust_decimal::serde::float")]
48    pub penalty_amount: Decimal,
49    #[serde(with = "rust_decimal::serde::float")]
50    pub total_amount: Decimal,
51    pub due_date: String,
52    pub days_overdue: i64,
53    pub delivery_method: DeliveryMethod,
54    pub sent_date: Option<String>,
55    pub opened_date: Option<String>,
56    pub pdf_path: Option<String>,
57    pub tracking_number: Option<String>,
58    pub notes: Option<String>,
59    pub created_at: String,
60    pub updated_at: String,
61}
62
63impl From<PaymentReminder> for PaymentReminderResponseDto {
64    fn from(reminder: PaymentReminder) -> Self {
65        Self {
66            id: reminder.id.to_string(),
67            acp_id: reminder.acp_id.to_string(),
68            organization_id: reminder.organization_id.to_string(),
69            expense_id: reminder.expense_id.to_string(),
70            owner_id: reminder.owner_id.to_string(),
71            owner_name: None,  // Will be enriched by use case
72            owner_email: None, // Will be enriched by use case
73            level: reminder.level,
74            status: reminder.status,
75            amount_owed: reminder.amount_owed,
76            penalty_amount: reminder.penalty_amount,
77            total_amount: reminder.total_amount,
78            due_date: reminder.due_date.to_rfc3339(),
79            days_overdue: reminder.days_overdue,
80            delivery_method: reminder.delivery_method,
81            sent_date: reminder.sent_date.map(|d| d.to_rfc3339()),
82            opened_date: reminder.opened_date.map(|d| d.to_rfc3339()),
83            pdf_path: reminder.pdf_path,
84            tracking_number: reminder.tracking_number,
85            notes: reminder.notes,
86            created_at: reminder.created_at.to_rfc3339(),
87            updated_at: reminder.updated_at.to_rfc3339(),
88        }
89    }
90}
91
92/// DTO for marking reminder as sent
93#[derive(Debug, Deserialize, Validate, Clone)]
94pub struct MarkReminderSentDto {
95    pub pdf_path: Option<String>,
96}
97
98/// DTO for escalating a reminder
99#[derive(Debug, Deserialize, Clone)]
100pub struct EscalateReminderDto {
101    pub reason: Option<String>,
102}
103
104/// DTO for cancelling a reminder
105#[derive(Debug, Deserialize, Validate, Clone)]
106pub struct CancelReminderDto {
107    #[validate(length(min = 1))]
108    pub reason: String,
109}
110
111/// DTO for adding tracking number
112#[derive(Debug, Deserialize, Validate, Clone)]
113pub struct AddTrackingNumberDto {
114    #[validate(length(min = 1))]
115    pub tracking_number: String,
116}
117
118/// DTO for payment recovery dashboard statistics
119#[derive(Debug, Serialize, Clone)]
120pub struct PaymentRecoveryStatsDto {
121    #[serde(with = "rust_decimal::serde::float")]
122    pub total_owed: Decimal,
123    #[serde(with = "rust_decimal::serde::float")]
124    pub total_penalties: Decimal,
125    pub reminder_counts: Vec<ReminderLevelCountDto>,
126    pub status_counts: Vec<ReminderStatusCountDto>,
127}
128
129#[derive(Debug, Serialize, Clone)]
130pub struct ReminderLevelCountDto {
131    pub level: ReminderLevel,
132    pub count: i64,
133}
134
135#[derive(Debug, Serialize, Clone)]
136pub struct ReminderStatusCountDto {
137    pub status: ReminderStatus,
138    pub count: i64,
139}
140
141/// DTO for overdue expense without reminder (for automated detection)
142#[derive(Debug, Serialize, Clone)]
143pub struct OverdueExpenseDto {
144    pub expense_id: String,
145    pub owner_id: String,
146    pub days_overdue: i64,
147    #[serde(with = "rust_decimal::serde::float")]
148    pub amount: Decimal,
149    pub recommended_level: ReminderLevel,
150}
151
152impl OverdueExpenseDto {
153    /// Create DTO with automatically determined reminder level
154    pub fn new(expense_id: String, owner_id: String, days_overdue: i64, amount: Decimal) -> Self {
155        let recommended_level = if days_overdue >= 60 {
156            ReminderLevel::FormalNotice
157        } else if days_overdue >= 30 {
158            ReminderLevel::SecondReminder
159        } else {
160            ReminderLevel::FirstReminder
161        };
162
163        Self {
164            expense_id,
165            owner_id,
166            days_overdue,
167            amount,
168            recommended_level,
169        }
170    }
171}
172
173/// DTO for bulk reminder creation
174#[derive(Debug, Deserialize, Validate, Clone)]
175pub struct BulkCreateRemindersDto {
176    #[serde(default)]
177    pub organization_id: String,
178
179    #[validate(range(min = 15))]
180    pub min_days_overdue: i64,
181}
182
183/// DTO for bulk reminder creation response
184#[derive(Debug, Serialize, Clone)]
185pub struct BulkCreateRemindersResponseDto {
186    pub created_count: i32,
187    pub skipped_count: i32,
188    pub errors: Vec<String>,
189    pub created_reminders: Vec<PaymentReminderResponseDto>,
190}