koprogo_api/infrastructure/database/repositories/
technical_spec_repository_impl.rs1use crate::application::error::AppError;
12use crate::application::ports::TechnicalSpecRepository;
13use crate::domain::entities::{
14 SemVer, SignatoryRole, TechnicalSpec, TechnicalSpecSignature, TechnicalSpecStatus,
15};
16use crate::infrastructure::database::pool::DbPool;
17use async_trait::async_trait;
18use chrono::{DateTime, Utc};
19use sqlx::Row;
20use std::str::FromStr;
21use uuid::Uuid;
22
23pub struct PostgresTechnicalSpecRepository {
24 pool: DbPool,
25}
26
27impl PostgresTechnicalSpecRepository {
28 pub fn new(pool: DbPool) -> Self {
29 Self { pool }
30 }
31
32 fn row_to_spec(row: &sqlx::postgres::PgRow) -> Result<TechnicalSpec, AppError> {
33 let status_str: String = row.get("status");
34 let status = TechnicalSpecStatus::from_str(&status_str)?;
35 let required_str: Vec<String> = row.get("required_signatures");
36 let required_signatures: Result<Vec<SignatoryRole>, AppError> = required_str
37 .iter()
38 .map(|r| SignatoryRole::from_str(r))
39 .collect();
40
41 let major: i32 = row.get("version_major");
42 let minor: i32 = row.get("version_minor");
43 let patch: i32 = row.get("version_patch");
44 let version = SemVer::new(major as u32, minor as u32, patch as u32);
45
46 Ok(TechnicalSpec {
47 id: row.get("id"),
48 acp_id: row.get("acp_id"),
49 building_id: row.get("building_id"),
50 title: row.get("title"),
51 description: row.get("description"),
52 version,
53 status,
54 deliverables: row.get("deliverables"),
55 required_signatures: required_signatures?,
56 attachments: row.get("attachments"),
57 previous_version_id: row.get("previous_version_id"),
58 created_by: row.get("created_by"),
59 created_at: row.get("created_at"),
60 updated_at: row.get("updated_at"),
61 })
62 }
63
64 fn row_to_signature(row: &sqlx::postgres::PgRow) -> Result<TechnicalSpecSignature, AppError> {
65 let role_str: String = row.get("role");
66 let role = SignatoryRole::from_str(&role_str)?;
67 Ok(TechnicalSpecSignature {
68 id: row.get("id"),
69 technical_spec_id: row.get("technical_spec_id"),
70 signatory_user_id: row.get("signatory_user_id"),
71 role,
72 mandate_id: row.get("mandate_id"),
73 signed_at: row.get("signed_at"),
74 })
75 }
76}
77
78#[async_trait]
79impl TechnicalSpecRepository for PostgresTechnicalSpecRepository {
80 async fn save(&self, spec: &TechnicalSpec) -> Result<(), AppError> {
81 let required_strs: Vec<String> = spec
82 .required_signatures
83 .iter()
84 .map(|r| r.to_string())
85 .collect();
86 sqlx::query(
87 r#"
88 INSERT INTO technical_specs (
89 id, acp_id, building_id, title, description,
90 version_major, version_minor, version_patch,
91 status, deliverables, required_signatures, attachments,
92 previous_version_id, created_by, created_at, updated_at
93 )
94 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
95 "#,
96 )
97 .bind(spec.id)
98 .bind(spec.acp_id)
99 .bind(spec.building_id)
100 .bind(&spec.title)
101 .bind(&spec.description)
102 .bind(spec.version.major as i32)
103 .bind(spec.version.minor as i32)
104 .bind(spec.version.patch as i32)
105 .bind(spec.status.to_string())
106 .bind(&spec.deliverables)
107 .bind(&required_strs)
108 .bind(&spec.attachments)
109 .bind(spec.previous_version_id)
110 .bind(spec.created_by)
111 .bind(spec.created_at)
112 .bind(spec.updated_at)
113 .execute(&self.pool)
114 .await
115 .map_err(|e| AppError::Database(e.to_string()))?;
116 Ok(())
117 }
118
119 async fn update_status(
120 &self,
121 spec_id: Uuid,
122 status: &str,
123 updated_at: DateTime<Utc>,
124 ) -> Result<(), AppError> {
125 sqlx::query(
126 r#"
127 UPDATE technical_specs
128 SET status = $2, updated_at = $3
129 WHERE id = $1
130 "#,
131 )
132 .bind(spec_id)
133 .bind(status)
134 .bind(updated_at)
135 .execute(&self.pool)
136 .await
137 .map_err(|e| AppError::Database(e.to_string()))?;
138 Ok(())
139 }
140
141 async fn find_by_id(&self, id: Uuid) -> Result<Option<TechnicalSpec>, AppError> {
142 let row = sqlx::query(
143 r#"
144 SELECT id, acp_id, building_id, title, description,
145 version_major, version_minor, version_patch,
146 status, deliverables, required_signatures, attachments,
147 previous_version_id, created_by, created_at, updated_at
148 FROM technical_specs
149 WHERE id = $1
150 "#,
151 )
152 .bind(id)
153 .fetch_optional(&self.pool)
154 .await
155 .map_err(|e| AppError::Database(e.to_string()))?;
156
157 match row {
158 None => Ok(None),
159 Some(row) => Ok(Some(Self::row_to_spec(&row)?)),
160 }
161 }
162
163 async fn list_for_acp(&self, acp_id: Uuid) -> Result<Vec<TechnicalSpec>, AppError> {
164 let rows = sqlx::query(
165 r#"
166 SELECT id, acp_id, building_id, title, description,
167 version_major, version_minor, version_patch,
168 status, deliverables, required_signatures, attachments,
169 previous_version_id, created_by, created_at, updated_at
170 FROM technical_specs
171 WHERE acp_id = $1
172 ORDER BY created_at DESC
173 "#,
174 )
175 .bind(acp_id)
176 .fetch_all(&self.pool)
177 .await
178 .map_err(|e| AppError::Database(e.to_string()))?;
179
180 rows.iter().map(Self::row_to_spec).collect()
181 }
182
183 async fn save_signature(&self, sig: &TechnicalSpecSignature) -> Result<(), AppError> {
184 let res = sqlx::query(
185 r#"
186 INSERT INTO technical_spec_signatures (
187 id, technical_spec_id, signatory_user_id, role, mandate_id, signed_at
188 )
189 VALUES ($1, $2, $3, $4, $5, $6)
190 "#,
191 )
192 .bind(sig.id)
193 .bind(sig.technical_spec_id)
194 .bind(sig.signatory_user_id)
195 .bind(sig.role.to_string())
196 .bind(sig.mandate_id)
197 .bind(sig.signed_at)
198 .execute(&self.pool)
199 .await;
200
201 match res {
202 Ok(_) => Ok(()),
203 Err(sqlx::Error::Database(db_err)) => {
204 if db_err.code().as_deref() == Some("23505") {
206 Err(AppError::SignatureAlreadyExists)
207 } else {
208 Err(AppError::Database(db_err.to_string()))
209 }
210 }
211 Err(e) => Err(AppError::Database(e.to_string())),
212 }
213 }
214
215 async fn list_signatures_for_spec(
216 &self,
217 spec_id: Uuid,
218 ) -> Result<Vec<TechnicalSpecSignature>, AppError> {
219 let rows = sqlx::query(
220 r#"
221 SELECT id, technical_spec_id, signatory_user_id, role, mandate_id, signed_at
222 FROM technical_spec_signatures
223 WHERE technical_spec_id = $1
224 ORDER BY signed_at ASC
225 "#,
226 )
227 .bind(spec_id)
228 .fetch_all(&self.pool)
229 .await
230 .map_err(|e| AppError::Database(e.to_string()))?;
231
232 rows.iter().map(Self::row_to_signature).collect()
233 }
234}