koprogo_api/infrastructure/database/repositories/
charge_distribution_repository_impl.rs1use crate::application::ports::ChargeDistributionRepository;
2use crate::domain::entities::ChargeDistribution;
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use rust_decimal::Decimal;
6use sqlx::PgPool;
7use uuid::Uuid;
8
9pub struct PostgresChargeDistributionRepository {
16 pool: PgPool,
17}
18
19impl PostgresChargeDistributionRepository {
20 pub fn new(pool: PgPool) -> Self {
21 Self { pool }
22 }
23}
24
25#[async_trait]
26impl ChargeDistributionRepository for PostgresChargeDistributionRepository {
27 async fn create(
28 &self,
29 distribution: &ChargeDistribution,
30 ) -> Result<ChargeDistribution, String> {
31 let result = sqlx::query_as::<_, ChargeDistributionRow>(
32 r#"
33 INSERT INTO charge_distributions (
34 id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
35 )
36 VALUES ($1, $2, $3, $4, $5, $6, $7)
37 RETURNING id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
38 "#,
39 )
40 .bind(distribution.id)
41 .bind(distribution.expense_id)
42 .bind(distribution.unit_id)
43 .bind(distribution.owner_id)
44 .bind(distribution.quota_percentage)
45 .bind(distribution.amount_due)
46 .bind(distribution.created_at)
47 .fetch_one(&self.pool)
48 .await
49 .map_err(|e| format!("Failed to create charge distribution: {}", e))?;
50
51 Ok(result.into_entity())
52 }
53
54 async fn create_bulk(
55 &self,
56 distributions: &[ChargeDistribution],
57 ) -> Result<Vec<ChargeDistribution>, String> {
58 if distributions.is_empty() {
59 return Ok(Vec::new());
60 }
61
62 let mut tx = self
64 .pool
65 .begin()
66 .await
67 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
68
69 let mut created = Vec::new();
70
71 for dist in distributions {
72 let result = sqlx::query_as::<_, ChargeDistributionRow>(
73 r#"
74 INSERT INTO charge_distributions (
75 id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
76 )
77 VALUES ($1, $2, $3, $4, $5, $6, $7)
78 RETURNING id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
79 "#
80 )
81 .bind(dist.id)
82 .bind(dist.expense_id)
83 .bind(dist.unit_id)
84 .bind(dist.owner_id)
85 .bind(dist.quota_percentage)
86 .bind(dist.amount_due)
87 .bind(dist.created_at)
88 .fetch_one(&mut *tx)
89 .await
90 .map_err(|e| format!("Failed to create charge distribution in bulk: {}", e))?;
91
92 created.push(result.into_entity());
93 }
94
95 tx.commit()
96 .await
97 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
98
99 Ok(created)
100 }
101
102 async fn find_by_id(&self, id: Uuid) -> Result<Option<ChargeDistribution>, String> {
103 let result = sqlx::query_as::<_, ChargeDistributionRow>(
104 r#"
105 SELECT id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
106 FROM charge_distributions
107 WHERE id = $1
108 "#,
109 )
110 .bind(id)
111 .fetch_optional(&self.pool)
112 .await
113 .map_err(|e| format!("Failed to find charge distribution by id: {}", e))?;
114
115 Ok(result.map(|r| r.into_entity()))
116 }
117
118 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<ChargeDistribution>, String> {
119 let results = sqlx::query_as::<_, ChargeDistributionRow>(
120 r#"
121 SELECT id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
122 FROM charge_distributions
123 WHERE expense_id = $1
124 ORDER BY created_at DESC
125 "#,
126 )
127 .bind(expense_id)
128 .fetch_all(&self.pool)
129 .await
130 .map_err(|e| format!("Failed to find charge distributions by expense: {}", e))?;
131
132 Ok(results.into_iter().map(|r| r.into_entity()).collect())
133 }
134
135 async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<ChargeDistribution>, String> {
136 let results = sqlx::query_as::<_, ChargeDistributionRow>(
137 r#"
138 SELECT id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
139 FROM charge_distributions
140 WHERE unit_id = $1
141 ORDER BY created_at DESC
142 "#,
143 )
144 .bind(unit_id)
145 .fetch_all(&self.pool)
146 .await
147 .map_err(|e| format!("Failed to find charge distributions by unit: {}", e))?;
148
149 Ok(results.into_iter().map(|r| r.into_entity()).collect())
150 }
151
152 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<ChargeDistribution>, String> {
153 let results = sqlx::query_as::<_, ChargeDistributionRow>(
154 r#"
155 SELECT id, expense_id, unit_id, owner_id, quota_percentage, amount_due, created_at
156 FROM charge_distributions
157 WHERE owner_id = $1
158 ORDER BY created_at DESC
159 "#,
160 )
161 .bind(owner_id)
162 .fetch_all(&self.pool)
163 .await
164 .map_err(|e| format!("Failed to find charge distributions by owner: {}", e))?;
165
166 Ok(results.into_iter().map(|r| r.into_entity()).collect())
167 }
168
169 async fn delete_by_expense(&self, expense_id: Uuid) -> Result<(), String> {
170 sqlx::query(
171 r#"
172 DELETE FROM charge_distributions
173 WHERE expense_id = $1
174 "#,
175 )
176 .bind(expense_id)
177 .execute(&self.pool)
178 .await
179 .map_err(|e| format!("Failed to delete charge distributions by expense: {}", e))?;
180
181 Ok(())
182 }
183
184 async fn get_total_due_by_owner(&self, owner_id: Uuid) -> Result<Decimal, String> {
185 let result: (Decimal,) = sqlx::query_as(
186 r#"
187 SELECT COALESCE(SUM(amount_due), 0)
188 FROM charge_distributions
189 WHERE owner_id = $1
190 "#,
191 )
192 .bind(owner_id)
193 .fetch_one(&self.pool)
194 .await
195 .map_err(|e| format!("Failed to get total due by owner: {}", e))?;
196
197 Ok(result.0)
198 }
199}
200
201#[derive(Debug, sqlx::FromRow)]
203struct ChargeDistributionRow {
204 id: Uuid,
205 expense_id: Uuid,
206 unit_id: Uuid,
207 owner_id: Uuid,
208 quota_percentage: Decimal,
209 amount_due: Decimal,
210 created_at: DateTime<Utc>,
211}
212
213impl ChargeDistributionRow {
214 fn into_entity(self) -> ChargeDistribution {
215 ChargeDistribution {
216 id: self.id,
217 expense_id: self.expense_id,
218 unit_id: self.unit_id,
219 owner_id: self.owner_id,
220 quota_percentage: self.quota_percentage,
221 amount_due: self.amount_due,
222 created_at: self.created_at,
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use rust_decimal_macros::dec;
231
232 #[test]
233 fn test_charge_distribution_row_to_entity() {
234 let row = ChargeDistributionRow {
235 id: Uuid::new_v4(),
236 expense_id: Uuid::new_v4(),
237 unit_id: Uuid::new_v4(),
238 owner_id: Uuid::new_v4(),
239 quota_percentage: Decimal::new(2500, 4), amount_due: Decimal::new(50000, 2), created_at: Utc::now(),
242 };
243
244 let entity = row.into_entity();
245 assert_eq!(entity.quota_percentage, dec!(0.2500));
246 assert_eq!(entity.amount_due, dec!(500.00));
247 }
248
249 #[test]
250 fn test_charge_distribution_row_to_entity_edge_cases() {
251 let row = ChargeDistributionRow {
252 id: Uuid::new_v4(),
253 expense_id: Uuid::new_v4(),
254 unit_id: Uuid::new_v4(),
255 owner_id: Uuid::new_v4(),
256 quota_percentage: Decimal::new(10000, 4), amount_due: Decimal::new(0, 2), created_at: Utc::now(),
259 };
260
261 let entity = row.into_entity();
262 assert_eq!(entity.quota_percentage, dec!(1.0000));
263 assert_eq!(entity.amount_due, dec!(0.00));
264 }
265}