Skip to main content

koprogo_api/infrastructure/database/repositories/
owner_contribution_repository_impl.rs

1use crate::application::ports::OwnerContributionRepository;
2use crate::domain::entities::{
3    ContributionPaymentMethod, ContributionPaymentStatus, ContributionType, OwnerContribution,
4};
5use async_trait::async_trait;
6use sqlx::PgPool;
7use uuid::Uuid;
8
9pub struct PostgresOwnerContributionRepository {
10    pool: PgPool,
11}
12
13impl PostgresOwnerContributionRepository {
14    pub fn new(pool: PgPool) -> Self {
15        Self { pool }
16    }
17
18    // Helper to convert DB row to domain entity
19    fn row_to_entity(row: sqlx::postgres::PgRow) -> Result<OwnerContribution, String> {
20        use sqlx::Row;
21
22        let contribution_type_str: String = row.get("contribution_type");
23        let contribution_type = match contribution_type_str.as_str() {
24            "regular" => ContributionType::Regular,
25            "extraordinary" => ContributionType::Extraordinary,
26            "advance" => ContributionType::Advance,
27            "adjustment" => ContributionType::Adjustment,
28            _ => {
29                return Err(format!(
30                    "Unknown contribution type: {}",
31                    contribution_type_str
32                ))
33            }
34        };
35
36        let payment_status_str: String = row.get("payment_status");
37        let payment_status = match payment_status_str.as_str() {
38            "pending" => ContributionPaymentStatus::Pending,
39            "paid" => ContributionPaymentStatus::Paid,
40            "partial" => ContributionPaymentStatus::Partial,
41            "cancelled" => ContributionPaymentStatus::Cancelled,
42            _ => return Err(format!("Unknown payment status: {}", payment_status_str)),
43        };
44
45        let payment_method: Option<String> = row.get("payment_method");
46        let payment_method = payment_method
47            .map(|pm| match pm.as_str() {
48                "bank_transfer" => Ok(ContributionPaymentMethod::BankTransfer),
49                "cash" => Ok(ContributionPaymentMethod::Cash),
50                "check" => Ok(ContributionPaymentMethod::Check),
51                "domiciliation" => Ok(ContributionPaymentMethod::Domiciliation),
52                _ => Err(format!("Unknown payment method: {}", pm)),
53            })
54            .transpose()?;
55
56        // amount stored as NUMERIC in DB; map directly to Decimal (cf. ADR-0007/0008)
57        let amount: sqlx::types::Decimal = row.get("amount");
58
59        Ok(OwnerContribution {
60            id: row.get("id"),
61            acp_id: row.get("acp_id"),
62            organization_id: row.get("organization_id"),
63            owner_id: row.get("owner_id"),
64            unit_id: row.get("unit_id"),
65            description: row.get("description"),
66            amount,
67            account_code: row.get("account_code"),
68            contribution_type,
69            contribution_date: row.get("contribution_date"),
70            payment_date: row.get("payment_date"),
71            payment_method,
72            payment_reference: row.get("payment_reference"),
73            payment_status,
74            call_for_funds_id: row.get("call_for_funds_id"),
75            notes: row.get("notes"),
76            created_at: row.get("created_at"),
77            updated_at: row.get("updated_at"),
78            created_by: row.get("created_by"),
79        })
80    }
81}
82
83#[async_trait]
84impl OwnerContributionRepository for PostgresOwnerContributionRepository {
85    async fn create(&self, contribution: &OwnerContribution) -> Result<OwnerContribution, String> {
86        let contribution_type = match contribution.contribution_type {
87            ContributionType::Regular => "regular",
88            ContributionType::Extraordinary => "extraordinary",
89            ContributionType::Advance => "advance",
90            ContributionType::Adjustment => "adjustment",
91        };
92
93        let payment_status = match contribution.payment_status {
94            ContributionPaymentStatus::Pending => "pending",
95            ContributionPaymentStatus::Paid => "paid",
96            ContributionPaymentStatus::Partial => "partial",
97            ContributionPaymentStatus::Cancelled => "cancelled",
98        };
99
100        let payment_method = contribution.payment_method.as_ref().map(|pm| match pm {
101            ContributionPaymentMethod::BankTransfer => "bank_transfer",
102            ContributionPaymentMethod::Cash => "cash",
103            ContributionPaymentMethod::Check => "check",
104            ContributionPaymentMethod::Domiciliation => "domiciliation",
105        });
106
107        let row = sqlx::query(
108            r#"
109            INSERT INTO owner_contributions (
110                id, acp_id, organization_id, owner_id, unit_id,
111                description, amount, account_code,
112                contribution_type, contribution_date, payment_date,
113                payment_method, payment_reference, payment_status,
114                call_for_funds_id, notes, created_at, updated_at, created_by
115            ) VALUES (
116                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
117            )
118            RETURNING *
119            "#,
120        )
121        .bind(contribution.id)
122        .bind(contribution.acp_id)
123        .bind(contribution.organization_id)
124        .bind(contribution.owner_id)
125        .bind(contribution.unit_id)
126        .bind(&contribution.description)
127        .bind(contribution.amount)
128        .bind(&contribution.account_code)
129        .bind(contribution_type)
130        .bind(contribution.contribution_date)
131        .bind(contribution.payment_date)
132        .bind(payment_method)
133        .bind(&contribution.payment_reference)
134        .bind(payment_status)
135        .bind(contribution.call_for_funds_id)
136        .bind(&contribution.notes)
137        .bind(contribution.created_at)
138        .bind(contribution.updated_at)
139        .bind(contribution.created_by)
140        .fetch_one(&self.pool)
141        .await
142        .map_err(|e| format!("Failed to create owner contribution: {}", e))?;
143
144        Self::row_to_entity(row)
145    }
146
147    async fn find_by_id(&self, id: Uuid) -> Result<Option<OwnerContribution>, String> {
148        let row = sqlx::query("SELECT * FROM owner_contributions WHERE id = $1")
149            .bind(id)
150            .fetch_optional(&self.pool)
151            .await
152            .map_err(|e| format!("Failed to find owner contribution by id: {}", e))?;
153
154        match row {
155            Some(r) => Ok(Some(Self::row_to_entity(r)?)),
156            None => Ok(None),
157        }
158    }
159
160    async fn find_by_organization(
161        &self,
162        organization_id: Uuid,
163    ) -> Result<Vec<OwnerContribution>, String> {
164        let rows = sqlx::query(
165            "SELECT * FROM owner_contributions WHERE acp_id IN (SELECT id FROM acps WHERE organization_id = $1) ORDER BY contribution_date DESC",
166        )
167        .bind(organization_id)
168        .fetch_all(&self.pool)
169        .await
170        .map_err(|e| format!("Failed to find owner contributions by organization: {}", e))?;
171
172        rows.into_iter().map(Self::row_to_entity).collect()
173    }
174
175    async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerContribution>, String> {
176        let rows = sqlx::query(
177            "SELECT * FROM owner_contributions WHERE owner_id = $1 ORDER BY contribution_date DESC",
178        )
179        .bind(owner_id)
180        .fetch_all(&self.pool)
181        .await
182        .map_err(|e| format!("Failed to find owner contributions by owner: {}", e))?;
183
184        rows.into_iter().map(Self::row_to_entity).collect()
185    }
186
187    async fn update(&self, contribution: &OwnerContribution) -> Result<OwnerContribution, String> {
188        let contribution_type = match contribution.contribution_type {
189            ContributionType::Regular => "regular",
190            ContributionType::Extraordinary => "extraordinary",
191            ContributionType::Advance => "advance",
192            ContributionType::Adjustment => "adjustment",
193        };
194
195        let payment_status = match contribution.payment_status {
196            ContributionPaymentStatus::Pending => "pending",
197            ContributionPaymentStatus::Paid => "paid",
198            ContributionPaymentStatus::Partial => "partial",
199            ContributionPaymentStatus::Cancelled => "cancelled",
200        };
201
202        let payment_method = contribution.payment_method.as_ref().map(|pm| match pm {
203            ContributionPaymentMethod::BankTransfer => "bank_transfer",
204            ContributionPaymentMethod::Cash => "cash",
205            ContributionPaymentMethod::Check => "check",
206            ContributionPaymentMethod::Domiciliation => "domiciliation",
207        });
208
209        let row = sqlx::query(
210            r#"
211            UPDATE owner_contributions SET
212                organization_id = $2,
213                owner_id = $3,
214                unit_id = $4,
215                description = $5,
216                amount = $6,
217                account_code = $7,
218                contribution_type = $8,
219                contribution_date = $9,
220                payment_date = $10,
221                payment_method = $11,
222                payment_reference = $12,
223                payment_status = $13,
224                notes = $14,
225                updated_at = $15,
226                created_by = $16
227            WHERE id = $1
228            RETURNING *
229            "#,
230        )
231        .bind(contribution.id)
232        .bind(contribution.organization_id)
233        .bind(contribution.owner_id)
234        .bind(contribution.unit_id)
235        .bind(&contribution.description)
236        .bind(contribution.amount)
237        .bind(&contribution.account_code)
238        .bind(contribution_type)
239        .bind(contribution.contribution_date)
240        .bind(contribution.payment_date)
241        .bind(payment_method)
242        .bind(&contribution.payment_reference)
243        .bind(payment_status)
244        .bind(&contribution.notes)
245        .bind(contribution.updated_at)
246        .bind(contribution.created_by)
247        .fetch_one(&self.pool)
248        .await
249        .map_err(|e| format!("Failed to update owner contribution: {}", e))?;
250
251        Self::row_to_entity(row)
252    }
253}