koprogo_api/infrastructure/database/repositories/
quote_repository_impl.rs

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(&quote.project_title)
38        .bind(&quote.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(&quote.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(&quote.project_title)
265        .bind(&quote.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(&quote.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
323/// Helper function to map PostgreSQL row to Quote entity
324fn 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}