1use crate::application::ports::AccountRepository;
14use crate::domain::entities::{Account, AccountType};
15use async_trait::async_trait;
16use sqlx::PgPool;
17use uuid::Uuid;
18
19pub struct PostgresAccountRepository {
24 pool: PgPool,
25}
26
27impl PostgresAccountRepository {
28 pub fn new(pool: PgPool) -> Self {
29 Self { pool }
30 }
31}
32
33#[async_trait]
34impl AccountRepository for PostgresAccountRepository {
35 async fn create(&self, account: &Account) -> Result<Account, String> {
36 let result = sqlx::query!(
37 r#"
38 INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
39 VALUES ($1, $2, $3, $4, $5::account_type, $6, $7, $8, $9)
40 RETURNING id, code, label, parent_code, account_type AS "account_type: AccountType",
41 direct_use, organization_id, created_at, updated_at
42 "#,
43 account.id,
44 &account.code,
45 &account.label,
46 account.parent_code.as_deref(),
47 account.account_type.clone() as AccountType,
48 account.direct_use,
49 account.organization_id,
50 account.created_at,
51 account.updated_at
52 )
53 .fetch_one(&self.pool)
54 .await
55 .map_err(|e| format!("Failed to create account: {}", e))?;
56
57 Ok(Account {
58 id: result.id,
59 code: result.code,
60 label: result.label,
61 parent_code: result.parent_code,
62 account_type: result.account_type,
63 direct_use: result.direct_use,
64 organization_id: result.organization_id,
65 created_at: result.created_at,
66 updated_at: result.updated_at,
67 })
68 }
69
70 async fn find_by_id(&self, id: Uuid) -> Result<Option<Account>, String> {
71 let result = sqlx::query!(
72 r#"
73 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
74 direct_use, organization_id, created_at, updated_at
75 FROM accounts
76 WHERE id = $1
77 "#,
78 id
79 )
80 .fetch_optional(&self.pool)
81 .await
82 .map_err(|e| format!("Failed to find account by id: {}", e))?;
83
84 Ok(result.map(|row| Account {
85 id: row.id,
86 code: row.code,
87 label: row.label,
88 parent_code: row.parent_code,
89 account_type: row.account_type,
90 direct_use: row.direct_use,
91 organization_id: row.organization_id,
92 created_at: row.created_at,
93 updated_at: row.updated_at,
94 }))
95 }
96
97 async fn find_by_code(
98 &self,
99 code: &str,
100 organization_id: Uuid,
101 ) -> Result<Option<Account>, String> {
102 let result = sqlx::query!(
103 r#"
104 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
105 direct_use, organization_id, created_at, updated_at
106 FROM accounts
107 WHERE code = $1 AND organization_id = $2
108 "#,
109 code,
110 organization_id
111 )
112 .fetch_optional(&self.pool)
113 .await
114 .map_err(|e| format!("Failed to find account by code: {}", e))?;
115
116 Ok(result.map(|row| Account {
117 id: row.id,
118 code: row.code,
119 label: row.label,
120 parent_code: row.parent_code,
121 account_type: row.account_type,
122 direct_use: row.direct_use,
123 organization_id: row.organization_id,
124 created_at: row.created_at,
125 updated_at: row.updated_at,
126 }))
127 }
128
129 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Account>, String> {
130 let rows = sqlx::query!(
131 r#"
132 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
133 direct_use, organization_id, created_at, updated_at
134 FROM accounts
135 WHERE organization_id = $1
136 ORDER BY code ASC
137 "#,
138 organization_id
139 )
140 .fetch_all(&self.pool)
141 .await
142 .map_err(|e| format!("Failed to find accounts by organization: {}", e))?;
143
144 Ok(rows
145 .into_iter()
146 .map(|row| Account {
147 id: row.id,
148 code: row.code,
149 label: row.label,
150 parent_code: row.parent_code,
151 account_type: row.account_type,
152 direct_use: row.direct_use,
153 organization_id: row.organization_id,
154 created_at: row.created_at,
155 updated_at: row.updated_at,
156 })
157 .collect())
158 }
159
160 async fn find_by_type(
161 &self,
162 account_type: AccountType,
163 organization_id: Uuid,
164 ) -> Result<Vec<Account>, String> {
165 let rows = sqlx::query!(
166 r#"
167 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
168 direct_use, organization_id, created_at, updated_at
169 FROM accounts
170 WHERE account_type = $1::account_type AND organization_id = $2
171 ORDER BY code ASC
172 "#,
173 account_type as AccountType,
174 organization_id
175 )
176 .fetch_all(&self.pool)
177 .await
178 .map_err(|e| format!("Failed to find accounts by type: {}", e))?;
179
180 Ok(rows
181 .into_iter()
182 .map(|row| Account {
183 id: row.id,
184 code: row.code,
185 label: row.label,
186 parent_code: row.parent_code,
187 account_type: row.account_type,
188 direct_use: row.direct_use,
189 organization_id: row.organization_id,
190 created_at: row.created_at,
191 updated_at: row.updated_at,
192 })
193 .collect())
194 }
195
196 async fn find_by_parent_code(
197 &self,
198 parent_code: &str,
199 organization_id: Uuid,
200 ) -> Result<Vec<Account>, String> {
201 let rows = sqlx::query!(
202 r#"
203 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
204 direct_use, organization_id, created_at, updated_at
205 FROM accounts
206 WHERE parent_code = $1 AND organization_id = $2
207 ORDER BY code ASC
208 "#,
209 parent_code,
210 organization_id
211 )
212 .fetch_all(&self.pool)
213 .await
214 .map_err(|e| format!("Failed to find accounts by parent code: {}", e))?;
215
216 Ok(rows
217 .into_iter()
218 .map(|row| Account {
219 id: row.id,
220 code: row.code,
221 label: row.label,
222 parent_code: row.parent_code,
223 account_type: row.account_type,
224 direct_use: row.direct_use,
225 organization_id: row.organization_id,
226 created_at: row.created_at,
227 updated_at: row.updated_at,
228 })
229 .collect())
230 }
231
232 async fn find_direct_use_accounts(
233 &self,
234 organization_id: Uuid,
235 ) -> Result<Vec<Account>, String> {
236 let rows = sqlx::query!(
237 r#"
238 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
239 direct_use, organization_id, created_at, updated_at
240 FROM accounts
241 WHERE direct_use = true AND organization_id = $1
242 ORDER BY code ASC
243 "#,
244 organization_id
245 )
246 .fetch_all(&self.pool)
247 .await
248 .map_err(|e| format!("Failed to find direct use accounts: {}", e))?;
249
250 Ok(rows
251 .into_iter()
252 .map(|row| Account {
253 id: row.id,
254 code: row.code,
255 label: row.label,
256 parent_code: row.parent_code,
257 account_type: row.account_type,
258 direct_use: row.direct_use,
259 organization_id: row.organization_id,
260 created_at: row.created_at,
261 updated_at: row.updated_at,
262 })
263 .collect())
264 }
265
266 async fn search_by_code_pattern(
267 &self,
268 code_pattern: &str,
269 organization_id: Uuid,
270 ) -> Result<Vec<Account>, String> {
271 let rows = sqlx::query!(
272 r#"
273 SELECT id, code, label, parent_code, account_type AS "account_type: AccountType",
274 direct_use, organization_id, created_at, updated_at
275 FROM accounts
276 WHERE code LIKE $1 AND organization_id = $2
277 ORDER BY code ASC
278 "#,
279 code_pattern,
280 organization_id
281 )
282 .fetch_all(&self.pool)
283 .await
284 .map_err(|e| format!("Failed to search accounts by code pattern: {}", e))?;
285
286 Ok(rows
287 .into_iter()
288 .map(|row| Account {
289 id: row.id,
290 code: row.code,
291 label: row.label,
292 parent_code: row.parent_code,
293 account_type: row.account_type,
294 direct_use: row.direct_use,
295 organization_id: row.organization_id,
296 created_at: row.created_at,
297 updated_at: row.updated_at,
298 })
299 .collect())
300 }
301
302 async fn update(&self, account: &Account) -> Result<Account, String> {
303 let result = sqlx::query!(
304 r#"
305 UPDATE accounts
306 SET code = $2, label = $3, parent_code = $4, account_type = $5::account_type,
307 direct_use = $6, updated_at = $7
308 WHERE id = $1
309 RETURNING id, code, label, parent_code, account_type AS "account_type: AccountType",
310 direct_use, organization_id, created_at, updated_at
311 "#,
312 account.id,
313 &account.code,
314 &account.label,
315 account.parent_code.as_deref(),
316 account.account_type.clone() as AccountType,
317 account.direct_use,
318 account.updated_at
319 )
320 .fetch_one(&self.pool)
321 .await
322 .map_err(|e| format!("Failed to update account: {}", e))?;
323
324 Ok(Account {
325 id: result.id,
326 code: result.code,
327 label: result.label,
328 parent_code: result.parent_code,
329 account_type: result.account_type,
330 direct_use: result.direct_use,
331 organization_id: result.organization_id,
332 created_at: result.created_at,
333 updated_at: result.updated_at,
334 })
335 }
336
337 async fn delete(&self, id: Uuid) -> Result<(), String> {
338 let has_children = sqlx::query!(
341 "SELECT COUNT(*) as count FROM accounts WHERE parent_code = (SELECT code FROM accounts WHERE id = $1)",
342 id
343 )
344 .fetch_one(&self.pool)
345 .await
346 .map_err(|e| format!("Failed to check for child accounts: {}", e))?;
347
348 if has_children.count.unwrap_or(0) > 0 {
349 return Err(
350 "Cannot delete account: it has child accounts. Delete children first.".to_string(),
351 );
352 }
353
354 let is_used = sqlx::query!(
356 "SELECT COUNT(*) as count FROM expenses WHERE account_code = (SELECT code FROM accounts WHERE id = $1)",
357 id
358 )
359 .fetch_one(&self.pool)
360 .await
361 .map_err(|e| format!("Failed to check account usage: {}", e))?;
362
363 if is_used.count.unwrap_or(0) > 0 {
364 return Err(
365 "Cannot delete account: it is used in expense transactions. Archive instead."
366 .to_string(),
367 );
368 }
369
370 let result = sqlx::query!("DELETE FROM accounts WHERE id = $1", id)
372 .execute(&self.pool)
373 .await
374 .map_err(|e| format!("Failed to delete account: {}", e))?;
375
376 if result.rows_affected() == 0 {
377 return Err("Account not found".to_string());
378 }
379
380 Ok(())
381 }
382
383 async fn exists(&self, code: &str, organization_id: Uuid) -> Result<bool, String> {
384 let result = sqlx::query!(
385 "SELECT COUNT(*) as count FROM accounts WHERE code = $1 AND organization_id = $2",
386 code,
387 organization_id
388 )
389 .fetch_one(&self.pool)
390 .await
391 .map_err(|e| format!("Failed to check account existence: {}", e))?;
392
393 Ok(result.count.unwrap_or(0) > 0)
394 }
395
396 async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String> {
397 let result = sqlx::query!(
398 "SELECT COUNT(*) as count FROM accounts WHERE organization_id = $1",
399 organization_id
400 )
401 .fetch_one(&self.pool)
402 .await
403 .map_err(|e| format!("Failed to count accounts: {}", e))?;
404
405 Ok(result.count.unwrap_or(0))
406 }
407}