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            organization_id: row.get("organization_id"),
62            owner_id: row.get("owner_id"),
63            unit_id: row.get("unit_id"),
64            description: row.get("description"),
65            amount,
66            account_code: row.get("account_code"),
67            contribution_type,
68            contribution_date: row.get("contribution_date"),
69            payment_date: row.get("payment_date"),
70            payment_method,
71            payment_reference: row.get("payment_reference"),
72            payment_status,
73            call_for_funds_id: row.get("call_for_funds_id"),
74            notes: row.get("notes"),
75            created_at: row.get("created_at"),
76            updated_at: row.get("updated_at"),
77            created_by: row.get("created_by"),
78        })
79    }
80}
81
82#[async_trait]
83impl OwnerContributionRepository for PostgresOwnerContributionRepository {
84    async fn create(&self, contribution: &OwnerContribution) -> Result<OwnerContribution, String> {
85        let contribution_type = match contribution.contribution_type {
86            ContributionType::Regular => "regular",
87            ContributionType::Extraordinary => "extraordinary",
88            ContributionType::Advance => "advance",
89            ContributionType::Adjustment => "adjustment",
90        };
91
92        let payment_status = match contribution.payment_status {
93            ContributionPaymentStatus::Pending => "pending",
94            ContributionPaymentStatus::Paid => "paid",
95            ContributionPaymentStatus::Partial => "partial",
96            ContributionPaymentStatus::Cancelled => "cancelled",
97        };
98
99        let payment_method = contribution.payment_method.as_ref().map(|pm| match pm {
100            ContributionPaymentMethod::BankTransfer => "bank_transfer",
101            ContributionPaymentMethod::Cash => "cash",
102            ContributionPaymentMethod::Check => "check",
103            ContributionPaymentMethod::Domiciliation => "domiciliation",
104        });
105
106        let row = sqlx::query(
107            r#"
108            INSERT INTO owner_contributions (
109                id, organization_id, owner_id, unit_id,
110                description, amount, account_code,
111                contribution_type, contribution_date, payment_date,
112                payment_method, payment_reference, payment_status,
113                call_for_funds_id, notes, created_at, updated_at, created_by
114            ) VALUES (
115                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
116            )
117            RETURNING *
118            "#,
119        )
120        .bind(contribution.id)
121        .bind(contribution.organization_id)
122        .bind(contribution.owner_id)
123        .bind(contribution.unit_id)
124        .bind(&contribution.description)
125        .bind(contribution.amount)
126        .bind(&contribution.account_code)
127        .bind(contribution_type)
128        .bind(contribution.contribution_date)
129        .bind(contribution.payment_date)
130        .bind(payment_method)
131        .bind(&contribution.payment_reference)
132        .bind(payment_status)
133        .bind(contribution.call_for_funds_id)
134        .bind(&contribution.notes)
135        .bind(contribution.created_at)
136        .bind(contribution.updated_at)
137        .bind(contribution.created_by)
138        .fetch_one(&self.pool)
139        .await
140        .map_err(|e| format!("Failed to create owner contribution: {}", e))?;
141
142        Self::row_to_entity(row)
143    }
144
145    async fn find_by_id(&self, id: Uuid) -> Result<Option<OwnerContribution>, String> {
146        let row = sqlx::query("SELECT * FROM owner_contributions WHERE id = $1")
147            .bind(id)
148            .fetch_optional(&self.pool)
149            .await
150            .map_err(|e| format!("Failed to find owner contribution by id: {}", e))?;
151
152        match row {
153            Some(r) => Ok(Some(Self::row_to_entity(r)?)),
154            None => Ok(None),
155        }
156    }
157
158    async fn find_by_organization(
159        &self,
160        organization_id: Uuid,
161    ) -> Result<Vec<OwnerContribution>, String> {
162        let rows = sqlx::query(
163            "SELECT * FROM owner_contributions WHERE organization_id = $1 ORDER BY contribution_date DESC",
164        )
165        .bind(organization_id)
166        .fetch_all(&self.pool)
167        .await
168        .map_err(|e| format!("Failed to find owner contributions by organization: {}", e))?;
169
170        rows.into_iter().map(Self::row_to_entity).collect()
171    }
172
173    async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerContribution>, String> {
174        let rows = sqlx::query(
175            "SELECT * FROM owner_contributions WHERE owner_id = $1 ORDER BY contribution_date DESC",
176        )
177        .bind(owner_id)
178        .fetch_all(&self.pool)
179        .await
180        .map_err(|e| format!("Failed to find owner contributions by owner: {}", e))?;
181
182        rows.into_iter().map(Self::row_to_entity).collect()
183    }
184
185    async fn update(&self, contribution: &OwnerContribution) -> Result<OwnerContribution, String> {
186        let contribution_type = match contribution.contribution_type {
187            ContributionType::Regular => "regular",
188            ContributionType::Extraordinary => "extraordinary",
189            ContributionType::Advance => "advance",
190            ContributionType::Adjustment => "adjustment",
191        };
192
193        let payment_status = match contribution.payment_status {
194            ContributionPaymentStatus::Pending => "pending",
195            ContributionPaymentStatus::Paid => "paid",
196            ContributionPaymentStatus::Partial => "partial",
197            ContributionPaymentStatus::Cancelled => "cancelled",
198        };
199
200        let payment_method = contribution.payment_method.as_ref().map(|pm| match pm {
201            ContributionPaymentMethod::BankTransfer => "bank_transfer",
202            ContributionPaymentMethod::Cash => "cash",
203            ContributionPaymentMethod::Check => "check",
204            ContributionPaymentMethod::Domiciliation => "domiciliation",
205        });
206
207        let row = sqlx::query(
208            r#"
209            UPDATE owner_contributions SET
210                organization_id = $2,
211                owner_id = $3,
212                unit_id = $4,
213                description = $5,
214                amount = $6,
215                account_code = $7,
216                contribution_type = $8,
217                contribution_date = $9,
218                payment_date = $10,
219                payment_method = $11,
220                payment_reference = $12,
221                payment_status = $13,
222                notes = $14,
223                updated_at = $15,
224                created_by = $16
225            WHERE id = $1
226            RETURNING *
227            "#,
228        )
229        .bind(contribution.id)
230        .bind(contribution.organization_id)
231        .bind(contribution.owner_id)
232        .bind(contribution.unit_id)
233        .bind(&contribution.description)
234        .bind(contribution.amount)
235        .bind(&contribution.account_code)
236        .bind(contribution_type)
237        .bind(contribution.contribution_date)
238        .bind(contribution.payment_date)
239        .bind(payment_method)
240        .bind(&contribution.payment_reference)
241        .bind(payment_status)
242        .bind(&contribution.notes)
243        .bind(contribution.updated_at)
244        .bind(contribution.created_by)
245        .fetch_one(&self.pool)
246        .await
247        .map_err(|e| format!("Failed to update owner contribution: {}", e))?;
248
249        Self::row_to_entity(row)
250    }
251}