koprogo_api/infrastructure/database/repositories/
fund_repository_impl.rs1use crate::application::error::AppError;
7use crate::application::ports::FundRepository;
8use crate::domain::entities::{Fund, FundKind, FundReassignment};
9use crate::infrastructure::database::pool::DbPool;
10use async_trait::async_trait;
11use sqlx::Row;
12use uuid::Uuid;
13
14pub struct PostgresFundRepository {
15 pool: DbPool,
16}
17
18impl PostgresFundRepository {
19 pub fn new(pool: DbPool) -> Self {
20 Self { pool }
21 }
22
23 fn kind_str(kind: FundKind) -> &'static str {
24 match kind {
25 FundKind::WorkingCapital => "working_capital",
26 FundKind::Reserve => "reserve",
27 FundKind::Earmarked => "earmarked",
28 }
29 }
30
31 fn row_to_fund(row: &sqlx::postgres::PgRow) -> Fund {
32 let kind_str: String = row.get("kind");
33 let kind = match kind_str.as_str() {
34 "reserve" => FundKind::Reserve,
35 "earmarked" => FundKind::Earmarked,
36 _ => FundKind::WorkingCapital,
37 };
38 Fund {
39 id: row.get("id"),
40 acp_id: row.get("acp_id"),
41 kind,
42 name: row.get("name"),
43 purpose: row.try_get("purpose").ok(),
44 target_amount: row.try_get("target_amount").ok(),
45 balance: row.get("balance"),
46 created_at: row.get("created_at"),
47 updated_at: row.get("updated_at"),
48 }
49 }
50
51 fn row_to_reassignment(row: &sqlx::postgres::PgRow) -> FundReassignment {
52 FundReassignment {
53 id: row.get("id"),
54 fund_id: row.get("fund_id"),
55 previous_purpose: row.get("previous_purpose"),
56 new_purpose: row.get("new_purpose"),
57 amount: row.get("amount"),
58 resolution_id: row.get("resolution_id"),
59 reassigned_at: row.get("reassigned_at"),
60 }
61 }
62}
63
64#[async_trait]
65impl FundRepository for PostgresFundRepository {
66 async fn create(&self, fund: &Fund) -> Result<Fund, AppError> {
67 sqlx::query(
68 r#"
69 INSERT INTO funds (
70 id, acp_id, kind, name, purpose, target_amount, balance,
71 created_at, updated_at
72 )
73 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
74 "#,
75 )
76 .bind(fund.id)
77 .bind(fund.acp_id)
78 .bind(Self::kind_str(fund.kind))
79 .bind(&fund.name)
80 .bind(&fund.purpose)
81 .bind(fund.target_amount)
82 .bind(fund.balance)
83 .bind(fund.created_at)
84 .bind(fund.updated_at)
85 .execute(&self.pool)
86 .await
87 .map_err(|e| AppError::Database(e.to_string()))?;
88
89 Ok(fund.clone())
90 }
91
92 async fn find_by_id(&self, id: Uuid) -> Result<Option<Fund>, AppError> {
93 let row = sqlx::query(
94 r#"
95 SELECT id, acp_id, kind, name, purpose, target_amount, balance,
96 created_at, updated_at
97 FROM funds
98 WHERE id = $1
99 "#,
100 )
101 .bind(id)
102 .fetch_optional(&self.pool)
103 .await
104 .map_err(|e| AppError::Database(e.to_string()))?;
105
106 Ok(row.map(|r| Self::row_to_fund(&r)))
107 }
108
109 async fn find_by_acp_id(&self, acp_id: Uuid) -> Result<Vec<Fund>, AppError> {
110 let rows = sqlx::query(
111 r#"
112 SELECT id, acp_id, kind, name, purpose, target_amount, balance,
113 created_at, updated_at
114 FROM funds
115 WHERE acp_id = $1
116 ORDER BY created_at ASC
117 "#,
118 )
119 .bind(acp_id)
120 .fetch_all(&self.pool)
121 .await
122 .map_err(|e| AppError::Database(e.to_string()))?;
123
124 Ok(rows.iter().map(Self::row_to_fund).collect())
125 }
126
127 async fn update(&self, fund: &Fund) -> Result<Fund, AppError> {
128 sqlx::query(
129 r#"
130 UPDATE funds
131 SET name = $2,
132 purpose = $3,
133 target_amount = $4,
134 balance = $5,
135 updated_at = $6
136 WHERE id = $1
137 "#,
138 )
139 .bind(fund.id)
140 .bind(&fund.name)
141 .bind(&fund.purpose)
142 .bind(fund.target_amount)
143 .bind(fund.balance)
144 .bind(fund.updated_at)
145 .execute(&self.pool)
146 .await
147 .map_err(|e| AppError::Database(e.to_string()))?;
148
149 Ok(fund.clone())
150 }
151
152 async fn record_reassignment(&self, reassignment: &FundReassignment) -> Result<(), AppError> {
153 sqlx::query(
154 r#"
155 INSERT INTO fund_reassignments (
156 id, fund_id, previous_purpose, new_purpose, amount,
157 resolution_id, reassigned_at
158 )
159 VALUES ($1, $2, $3, $4, $5, $6, $7)
160 "#,
161 )
162 .bind(reassignment.id)
163 .bind(reassignment.fund_id)
164 .bind(&reassignment.previous_purpose)
165 .bind(&reassignment.new_purpose)
166 .bind(reassignment.amount)
167 .bind(reassignment.resolution_id)
168 .bind(reassignment.reassigned_at)
169 .execute(&self.pool)
170 .await
171 .map_err(|e| AppError::Database(e.to_string()))?;
172
173 Ok(())
174 }
175
176 async fn list_reassignments(&self, fund_id: Uuid) -> Result<Vec<FundReassignment>, AppError> {
177 let rows = sqlx::query(
178 r#"
179 SELECT id, fund_id, previous_purpose, new_purpose, amount,
180 resolution_id, reassigned_at
181 FROM fund_reassignments
182 WHERE fund_id = $1
183 ORDER BY reassigned_at DESC
184 "#,
185 )
186 .bind(fund_id)
187 .fetch_all(&self.pool)
188 .await
189 .map_err(|e| AppError::Database(e.to_string()))?;
190
191 Ok(rows.iter().map(Self::row_to_reassignment).collect())
192 }
193}