1use crate::domain::entities::{Quote, QuoteScore, QuoteSubmission};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4
5fn default_warranty_years() -> i32 {
6 2
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CreateQuoteDto {
21 pub building_id: String,
22 pub contractor_id: String,
23 pub project_title: String,
24 pub project_description: String,
25 #[serde(default)]
26 pub work_category: Option<String>,
27 #[serde(default)]
28 pub amount_excl_vat: Option<Decimal>,
29 #[serde(default)]
30 pub vat_rate: Option<Decimal>,
31 #[serde(default)]
32 pub validity_date: Option<String>, pub estimated_start_date: Option<String>,
34 #[serde(default)]
35 pub estimated_duration_days: Option<i32>,
36 #[serde(default = "default_warranty_years")]
37 pub warranty_years: i32,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SubmitQuoteDto {
48 pub amount_excl_vat_cents: i64,
49 pub vat_rate: Decimal, pub validity_date: String,
51 pub estimated_duration_days: i32,
52 pub warranty_years: i32,
53}
54
55impl SubmitQuoteDto {
56 pub fn into_domain(self) -> Result<QuoteSubmission, String> {
57 let validity_date = chrono::DateTime::parse_from_rfc3339(&self.validity_date)
58 .map_err(|_| "Invalid validity_date format".to_string())?
59 .with_timezone(&chrono::Utc);
60
61 Ok(QuoteSubmission {
62 amount_excl_vat: Decimal::from(self.amount_excl_vat_cents) / Decimal::from(100),
63 vat_rate: self.vat_rate / Decimal::from(100),
64 validity_date,
65 estimated_duration_days: self.estimated_duration_days,
66 warranty_years: self.warranty_years,
67 })
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct QuoteDecisionDto {
74 pub decision_notes: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct QuoteComparisonRequestDto {
80 pub quote_ids: Vec<String>, }
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct QuoteResponseDto {
90 pub id: String,
91 pub building_id: String,
92 pub contractor_id: String,
93 pub project_title: String,
94 pub project_description: String,
95 pub work_category: Option<String>,
96
97 pub amount_excl_vat_cents: Option<i64>,
99 pub vat_rate: Option<Decimal>, pub amount_incl_vat_cents: Option<i64>,
101 pub validity_date: Option<String>,
102 pub estimated_start_date: Option<String>,
103 pub estimated_duration_days: Option<i32>,
104
105 pub warranty_years: i32,
107 pub contractor_rating: Option<i32>,
108
109 pub status: String,
111 pub is_expired: bool,
112
113 pub requested_at: String,
115 pub submitted_at: Option<String>,
116 pub reviewed_at: Option<String>,
117 pub decision_at: Option<String>,
118 pub decision_by: Option<String>,
119 pub decision_notes: Option<String>,
120
121 pub created_at: String,
123 pub updated_at: String,
124}
125
126fn to_cents(amount: Decimal) -> i64 {
128 use rust_decimal::prelude::ToPrimitive;
129 (amount * Decimal::from(100)).round().to_i64().unwrap_or(0)
130}
131
132impl From<Quote> for QuoteResponseDto {
133 fn from(quote: Quote) -> Self {
134 let is_expired = quote.is_expired();
135 Self {
136 id: quote.id.to_string(),
137 building_id: quote.building_id.to_string(),
138 contractor_id: quote.contractor_id.to_string(),
139 project_title: quote.project_title.clone(),
140 project_description: quote.project_description.clone(),
141 work_category: quote.work_category.clone(),
142 amount_excl_vat_cents: quote.amount_excl_vat.map(to_cents),
143 vat_rate: quote.vat_rate.map(|r| r * Decimal::from(100)),
144 amount_incl_vat_cents: quote.amount_incl_vat.map(to_cents),
145 validity_date: quote.validity_date.map(|d| d.to_rfc3339()),
146 estimated_start_date: quote.estimated_start_date.map(|d| d.to_rfc3339()),
147 estimated_duration_days: quote.estimated_duration_days,
148 warranty_years: quote.warranty_years,
149 contractor_rating: quote.contractor_rating,
150 status: quote.status.to_sql().to_string(),
151 is_expired,
152 requested_at: quote.requested_at.to_rfc3339(),
153 submitted_at: quote.submitted_at.map(|d| d.to_rfc3339()),
154 reviewed_at: quote.reviewed_at.map(|d| d.to_rfc3339()),
155 decision_at: quote.decision_at.map(|d| d.to_rfc3339()),
156 decision_by: quote.decision_by.map(|u| u.to_string()),
157 decision_notes: quote.decision_notes,
158 created_at: quote.created_at.to_rfc3339(),
159 updated_at: quote.updated_at.to_rfc3339(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct QuoteScoreResponseDto {
167 pub quote_id: String,
168 pub total_score: f32,
169 pub price_score: f32,
170 pub delay_score: f32,
171 pub warranty_score: f32,
172 pub reputation_score: f32,
173}
174
175impl From<QuoteScore> for QuoteScoreResponseDto {
176 fn from(score: QuoteScore) -> Self {
177 Self {
178 quote_id: score.quote_id.to_string(),
179 total_score: score.total_score,
180 price_score: score.price_score,
181 delay_score: score.delay_score,
182 warranty_score: score.warranty_score,
183 reputation_score: score.reputation_score,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct QuoteComparisonItemDto {
191 pub quote: QuoteResponseDto,
192 pub score: Option<QuoteScoreResponseDto>,
193 pub rank: usize, }
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct QuoteComparisonResponseDto {
199 pub project_title: String,
200 pub building_id: String,
201 pub total_quotes: usize,
202 pub comparison_items: Vec<QuoteComparisonItemDto>,
203
204 pub min_price: String, pub max_price: String,
207 pub avg_price: String,
208 pub min_duration_days: i32,
209 pub max_duration_days: i32,
210 pub avg_duration_days: f32,
211
212 pub recommended_quote_id: Option<String>,
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::domain::entities::Quote;
220 use chrono::Utc;
221 use rust_decimal::Decimal;
222 use std::str::FromStr;
223 use uuid::Uuid;
224
225 macro_rules! dec {
227 ($val:expr) => {
228 Decimal::from_str(stringify!($val)).unwrap()
229 };
230 }
231
232 #[test]
233 fn test_quote_response_dto_conversion_unpriced() {
234 let building_id = Uuid::new_v4();
235 let contractor_id = Uuid::new_v4();
236
237 let quote = Quote::new(
238 building_id,
239 contractor_id,
240 "Roof Repair".to_string(),
241 "Repair leaking roof tiles".to_string(),
242 Some("roofing".to_string()),
243 10,
244 )
245 .unwrap();
246
247 let dto = QuoteResponseDto::from(quote.clone());
248
249 assert_eq!(dto.id, quote.id.to_string());
250 assert_eq!(dto.project_title, "Roof Repair");
251 assert_eq!(dto.amount_excl_vat_cents, None);
252 assert_eq!(dto.amount_incl_vat_cents, None);
253 assert_eq!(dto.status, "Requested");
254 assert!(!dto.is_expired);
255 assert_eq!(dto.warranty_years, 10);
256 }
257
258 #[test]
259 fn test_quote_response_dto_conversion_priced() {
260 let building_id = Uuid::new_v4();
261 let contractor_id = Uuid::new_v4();
262 let validity_date = Utc::now() + chrono::Duration::days(30);
263
264 let mut quote = Quote::new(
265 building_id,
266 contractor_id,
267 "Roof Repair".to_string(),
268 "Repair leaking roof tiles".to_string(),
269 Some("roofing".to_string()),
270 10,
271 )
272 .unwrap();
273 quote
274 .submit(Some(QuoteSubmission {
275 amount_excl_vat: dec!(5000.00),
276 vat_rate: dec!(0.21),
277 validity_date,
278 estimated_duration_days: 14,
279 warranty_years: 10,
280 }))
281 .unwrap();
282
283 let dto = QuoteResponseDto::from(quote.clone());
284
285 assert_eq!(dto.amount_excl_vat_cents, Some(500_000));
286 assert_eq!(dto.amount_incl_vat_cents, Some(605_000));
287 assert_eq!(dto.vat_rate, Some(dec!(21.00)));
288 assert_eq!(dto.status, "Received");
289 assert_eq!(dto.estimated_duration_days, Some(14));
290 assert_eq!(dto.warranty_years, 10);
291 }
292
293 #[test]
294 fn test_quote_score_dto_conversion() {
295 let quote_id = Uuid::new_v4();
296 let score = QuoteScore {
297 quote_id,
298 total_score: 75.5,
299 price_score: 80.0,
300 delay_score: 70.0,
301 warranty_score: 90.0,
302 reputation_score: 60.0,
303 };
304
305 let dto = QuoteScoreResponseDto::from(score.clone());
306
307 assert_eq!(dto.quote_id, quote_id.to_string());
308 assert_eq!(dto.total_score, 75.5);
309 assert_eq!(dto.price_score, 80.0);
310 assert_eq!(dto.delay_score, 70.0);
311 assert_eq!(dto.warranty_score, 90.0);
312 assert_eq!(dto.reputation_score, 60.0);
313 }
314}