1use crate::application::ports::QuoteRepository;
2use crate::domain::entities::{Quote, QuoteStatus};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresQuoteRepository {
9 pool: DbPool,
10}
11
12impl PostgresQuoteRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16}
17
18#[async_trait]
19impl QuoteRepository for PostgresQuoteRepository {
20 async fn create(&self, quote: &Quote) -> Result<Quote, String> {
21 sqlx::query(
22 r#"
23 INSERT INTO quotes (
24 id, building_id, contractor_id, project_title, project_description,
25 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
26 estimated_start_date, estimated_duration_days, warranty_years,
27 contractor_rating, status, requested_at, submitted_at,
28 reviewed_at, decision_at, decision_by, decision_notes,
29 created_at, updated_at
30 )
31 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
32 "#,
33 )
34 .bind(quote.id)
35 .bind(quote.building_id)
36 .bind(quote.contractor_id)
37 .bind("e.project_title)
38 .bind("e.project_description)
39 .bind(quote.amount_excl_vat)
40 .bind(quote.vat_rate)
41 .bind(quote.amount_incl_vat)
42 .bind(quote.validity_date)
43 .bind(quote.estimated_start_date)
44 .bind(quote.estimated_duration_days)
45 .bind(quote.warranty_years)
46 .bind(quote.contractor_rating)
47 .bind(quote.status.to_sql())
48 .bind(quote.requested_at)
49 .bind(quote.submitted_at)
50 .bind(quote.reviewed_at)
51 .bind(quote.decision_at)
52 .bind(quote.decision_by)
53 .bind("e.decision_notes)
54 .bind(quote.created_at)
55 .bind(quote.updated_at)
56 .execute(&self.pool)
57 .await
58 .map_err(|e| format!("Database error: {}", e))?;
59
60 Ok(quote.clone())
61 }
62
63 async fn find_by_id(&self, id: Uuid) -> Result<Option<Quote>, String> {
64 let row = sqlx::query(
65 r#"
66 SELECT
67 id, building_id, contractor_id, project_title, project_description,
68 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
69 estimated_start_date, estimated_duration_days, warranty_years,
70 contractor_rating, status, requested_at, submitted_at,
71 reviewed_at, decision_at, decision_by, decision_notes,
72 created_at, updated_at
73 FROM quotes
74 WHERE id = $1
75 "#,
76 )
77 .bind(id)
78 .fetch_optional(&self.pool)
79 .await
80 .map_err(|e| format!("Database error: {}", e))?;
81
82 Ok(row.map(|row| map_row_to_quote(&row)))
83 }
84
85 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Quote>, String> {
86 let rows = sqlx::query(
87 r#"
88 SELECT
89 id, building_id, contractor_id, project_title, project_description,
90 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
91 estimated_start_date, estimated_duration_days, warranty_years,
92 contractor_rating, status, requested_at, submitted_at,
93 reviewed_at, decision_at, decision_by, decision_notes,
94 created_at, updated_at
95 FROM quotes
96 WHERE building_id = $1
97 ORDER BY requested_at DESC
98 "#,
99 )
100 .bind(building_id)
101 .fetch_all(&self.pool)
102 .await
103 .map_err(|e| format!("Database error: {}", e))?;
104
105 Ok(rows.iter().map(map_row_to_quote).collect())
106 }
107
108 async fn find_by_contractor(&self, contractor_id: Uuid) -> Result<Vec<Quote>, String> {
109 let rows = sqlx::query(
110 r#"
111 SELECT
112 id, building_id, contractor_id, project_title, project_description,
113 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
114 estimated_start_date, estimated_duration_days, warranty_years,
115 contractor_rating, status, requested_at, submitted_at,
116 reviewed_at, decision_at, decision_by, decision_notes,
117 created_at, updated_at
118 FROM quotes
119 WHERE contractor_id = $1
120 ORDER BY requested_at DESC
121 "#,
122 )
123 .bind(contractor_id)
124 .fetch_all(&self.pool)
125 .await
126 .map_err(|e| format!("Database error: {}", e))?;
127
128 Ok(rows.iter().map(map_row_to_quote).collect())
129 }
130
131 async fn find_by_status(&self, building_id: Uuid, status: &str) -> Result<Vec<Quote>, String> {
132 let rows = sqlx::query(
133 r#"
134 SELECT
135 id, building_id, contractor_id, project_title, project_description,
136 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
137 estimated_start_date, estimated_duration_days, warranty_years,
138 contractor_rating, status, requested_at, submitted_at,
139 reviewed_at, decision_at, decision_by, decision_notes,
140 created_at, updated_at
141 FROM quotes
142 WHERE building_id = $1 AND status = $2
143 ORDER BY requested_at DESC
144 "#,
145 )
146 .bind(building_id)
147 .bind(status)
148 .fetch_all(&self.pool)
149 .await
150 .map_err(|e| format!("Database error: {}", e))?;
151
152 Ok(rows.iter().map(map_row_to_quote).collect())
153 }
154
155 async fn find_by_ids(&self, ids: Vec<Uuid>) -> Result<Vec<Quote>, String> {
156 if ids.is_empty() {
157 return Ok(vec![]);
158 }
159
160 let rows = sqlx::query(
161 r#"
162 SELECT
163 id, building_id, contractor_id, project_title, project_description,
164 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
165 estimated_start_date, estimated_duration_days, warranty_years,
166 contractor_rating, status, requested_at, submitted_at,
167 reviewed_at, decision_at, decision_by, decision_notes,
168 created_at, updated_at
169 FROM quotes
170 WHERE id = ANY($1)
171 ORDER BY amount_incl_vat ASC
172 "#,
173 )
174 .bind(&ids)
175 .fetch_all(&self.pool)
176 .await
177 .map_err(|e| format!("Database error: {}", e))?;
178
179 Ok(rows.iter().map(map_row_to_quote).collect())
180 }
181
182 async fn find_by_project_title(
183 &self,
184 building_id: Uuid,
185 project_title: &str,
186 ) -> Result<Vec<Quote>, String> {
187 let rows = sqlx::query(
188 r#"
189 SELECT
190 id, building_id, contractor_id, project_title, project_description,
191 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
192 estimated_start_date, estimated_duration_days, warranty_years,
193 contractor_rating, status, requested_at, submitted_at,
194 reviewed_at, decision_at, decision_by, decision_notes,
195 created_at, updated_at
196 FROM quotes
197 WHERE building_id = $1 AND project_title ILIKE $2
198 ORDER BY requested_at DESC
199 "#,
200 )
201 .bind(building_id)
202 .bind(format!("%{}%", project_title))
203 .fetch_all(&self.pool)
204 .await
205 .map_err(|e| format!("Database error: {}", e))?;
206
207 Ok(rows.iter().map(map_row_to_quote).collect())
208 }
209
210 async fn find_expired(&self) -> Result<Vec<Quote>, String> {
211 let rows = sqlx::query(
212 r#"
213 SELECT
214 id, building_id, contractor_id, project_title, project_description,
215 amount_excl_vat, vat_rate, amount_incl_vat, validity_date,
216 estimated_start_date, estimated_duration_days, warranty_years,
217 contractor_rating, status, requested_at, submitted_at,
218 reviewed_at, decision_at, decision_by, decision_notes,
219 created_at, updated_at
220 FROM quotes
221 WHERE validity_date < NOW()
222 AND status NOT IN ('Accepted', 'Rejected', 'Expired', 'Withdrawn')
223 ORDER BY validity_date ASC
224 "#,
225 )
226 .fetch_all(&self.pool)
227 .await
228 .map_err(|e| format!("Database error: {}", e))?;
229
230 Ok(rows.iter().map(map_row_to_quote).collect())
231 }
232
233 async fn update(&self, quote: &Quote) -> Result<Quote, String> {
234 sqlx::query(
235 r#"
236 UPDATE quotes
237 SET
238 building_id = $2,
239 contractor_id = $3,
240 project_title = $4,
241 project_description = $5,
242 amount_excl_vat = $6,
243 vat_rate = $7,
244 amount_incl_vat = $8,
245 validity_date = $9,
246 estimated_start_date = $10,
247 estimated_duration_days = $11,
248 warranty_years = $12,
249 contractor_rating = $13,
250 status = $14,
251 requested_at = $15,
252 submitted_at = $16,
253 reviewed_at = $17,
254 decision_at = $18,
255 decision_by = $19,
256 decision_notes = $20,
257 updated_at = $21
258 WHERE id = $1
259 "#,
260 )
261 .bind(quote.id)
262 .bind(quote.building_id)
263 .bind(quote.contractor_id)
264 .bind("e.project_title)
265 .bind("e.project_description)
266 .bind(quote.amount_excl_vat)
267 .bind(quote.vat_rate)
268 .bind(quote.amount_incl_vat)
269 .bind(quote.validity_date)
270 .bind(quote.estimated_start_date)
271 .bind(quote.estimated_duration_days)
272 .bind(quote.warranty_years)
273 .bind(quote.contractor_rating)
274 .bind(quote.status.to_sql())
275 .bind(quote.requested_at)
276 .bind(quote.submitted_at)
277 .bind(quote.reviewed_at)
278 .bind(quote.decision_at)
279 .bind(quote.decision_by)
280 .bind("e.decision_notes)
281 .bind(quote.updated_at)
282 .execute(&self.pool)
283 .await
284 .map_err(|e| format!("Database error: {}", e))?;
285
286 Ok(quote.clone())
287 }
288
289 async fn delete(&self, id: Uuid) -> Result<bool, String> {
290 let result = sqlx::query("DELETE FROM quotes WHERE id = $1")
291 .bind(id)
292 .execute(&self.pool)
293 .await
294 .map_err(|e| format!("Database error: {}", e))?;
295
296 Ok(result.rows_affected() > 0)
297 }
298
299 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
300 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM quotes WHERE building_id = $1")
301 .bind(building_id)
302 .fetch_one(&self.pool)
303 .await
304 .map_err(|e| format!("Database error: {}", e))?;
305
306 Ok(count)
307 }
308
309 async fn count_by_status(&self, building_id: Uuid, status: &str) -> Result<i64, String> {
310 let count: i64 = sqlx::query_scalar(
311 "SELECT COUNT(*) FROM quotes WHERE building_id = $1 AND status = $2",
312 )
313 .bind(building_id)
314 .bind(status)
315 .fetch_one(&self.pool)
316 .await
317 .map_err(|e| format!("Database error: {}", e))?;
318
319 Ok(count)
320 }
321}
322
323fn map_row_to_quote(row: &sqlx::postgres::PgRow) -> Quote {
325 Quote {
326 id: row.get("id"),
327 building_id: row.get("building_id"),
328 contractor_id: row.get("contractor_id"),
329 project_title: row.get("project_title"),
330 project_description: row.get("project_description"),
331 amount_excl_vat: row.get("amount_excl_vat"),
332 vat_rate: row.get("vat_rate"),
333 amount_incl_vat: row.get("amount_incl_vat"),
334 validity_date: row.get("validity_date"),
335 estimated_start_date: row.get("estimated_start_date"),
336 estimated_duration_days: row.get("estimated_duration_days"),
337 warranty_years: row.get("warranty_years"),
338 contractor_rating: row.get("contractor_rating"),
339 status: QuoteStatus::from_sql(row.get("status")).unwrap_or(QuoteStatus::Requested),
340 requested_at: row.get("requested_at"),
341 submitted_at: row.get("submitted_at"),
342 reviewed_at: row.get("reviewed_at"),
343 decision_at: row.get("decision_at"),
344 decision_by: row.get("decision_by"),
345 decision_notes: row.get("decision_notes"),
346 created_at: row.get("created_at"),
347 updated_at: row.get("updated_at"),
348 }
349}