koprogo_api/application/use_cases/
call_for_funds_use_cases.rs

1use crate::application::ports::{
2    CallForFundsRepository, OwnerContributionRepository, UnitOwnerRepository,
3};
4use crate::domain::entities::{CallForFunds, ContributionType, OwnerContribution};
5use chrono::{DateTime, Utc};
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct CallForFundsUseCases {
10    call_for_funds_repository: Arc<dyn CallForFundsRepository>,
11    owner_contribution_repository: Arc<dyn OwnerContributionRepository>,
12    unit_owner_repository: Arc<dyn UnitOwnerRepository>,
13}
14
15impl CallForFundsUseCases {
16    pub fn new(
17        call_for_funds_repository: Arc<dyn CallForFundsRepository>,
18        owner_contribution_repository: Arc<dyn OwnerContributionRepository>,
19        unit_owner_repository: Arc<dyn UnitOwnerRepository>,
20    ) -> Self {
21        Self {
22            call_for_funds_repository,
23            owner_contribution_repository,
24            unit_owner_repository,
25        }
26    }
27
28    /// Create a new call for funds
29    #[allow(clippy::too_many_arguments)]
30    pub async fn create_call_for_funds(
31        &self,
32        organization_id: Uuid,
33        building_id: Uuid,
34        title: String,
35        description: String,
36        total_amount: f64,
37        contribution_type: ContributionType,
38        call_date: DateTime<Utc>,
39        due_date: DateTime<Utc>,
40        account_code: Option<String>,
41        created_by: Option<Uuid>,
42    ) -> Result<CallForFunds, String> {
43        // Create the call for funds entity
44        let mut call_for_funds = CallForFunds::new(
45            organization_id,
46            building_id,
47            title,
48            description,
49            total_amount,
50            contribution_type.clone(),
51            call_date,
52            due_date,
53            account_code,
54        )?;
55
56        call_for_funds.created_by = created_by;
57
58        // Save to database
59        self.call_for_funds_repository.create(&call_for_funds).await
60    }
61
62    /// Get a call for funds by ID
63    pub async fn get_call_for_funds(&self, id: Uuid) -> Result<Option<CallForFunds>, String> {
64        self.call_for_funds_repository.find_by_id(id).await
65    }
66
67    /// List all calls for funds for a building
68    pub async fn list_by_building(&self, building_id: Uuid) -> Result<Vec<CallForFunds>, String> {
69        self.call_for_funds_repository
70            .find_by_building(building_id)
71            .await
72    }
73
74    /// List all calls for funds for an organization
75    pub async fn list_by_organization(
76        &self,
77        organization_id: Uuid,
78    ) -> Result<Vec<CallForFunds>, String> {
79        self.call_for_funds_repository
80            .find_by_organization(organization_id)
81            .await
82    }
83
84    /// Mark call for funds as sent and generate individual owner contributions
85    /// This is the key operation that automatically creates contributions for all owners
86    pub async fn send_call_for_funds(&self, id: Uuid) -> Result<CallForFunds, String> {
87        // Get the call for funds
88        let mut call_for_funds = self
89            .call_for_funds_repository
90            .find_by_id(id)
91            .await?
92            .ok_or_else(|| "Call for funds not found".to_string())?;
93
94        // Mark as sent
95        call_for_funds.mark_as_sent();
96
97        // Update in database
98        let updated_call = self
99            .call_for_funds_repository
100            .update(&call_for_funds)
101            .await?;
102
103        // Generate individual contributions for all owners in the building
104        self.generate_owner_contributions(&updated_call).await?;
105
106        Ok(updated_call)
107    }
108
109    /// Generate individual owner contributions based on ownership percentages
110    async fn generate_owner_contributions(
111        &self,
112        call_for_funds: &CallForFunds,
113    ) -> Result<Vec<OwnerContribution>, String> {
114        // Get all active unit owners for the building
115        // Returns (unit_id, owner_id, percentage)
116        let unit_owners = self
117            .unit_owner_repository
118            .find_active_by_building(call_for_funds.building_id)
119            .await?;
120
121        if unit_owners.is_empty() {
122            return Err("No active owners found for this building".to_string());
123        }
124
125        let mut contributions = Vec::new();
126
127        for (unit_id, owner_id, percentage) in unit_owners {
128            // Calculate individual amount based on ownership percentage
129            let individual_amount = call_for_funds.total_amount * percentage;
130
131            // Create contribution description
132            let description = format!(
133                "{} - Quote-part: {:.2}%",
134                call_for_funds.title,
135                percentage * 100.0
136            );
137
138            // Create owner contribution
139            let mut contribution = OwnerContribution::new(
140                call_for_funds.organization_id,
141                owner_id,
142                Some(unit_id),
143                description,
144                individual_amount,
145                call_for_funds.contribution_type.clone(),
146                call_for_funds.call_date,
147                call_for_funds.account_code.clone(),
148            )?;
149
150            // Link to the call for funds
151            contribution.call_for_funds_id = Some(call_for_funds.id);
152
153            // Save contribution
154            let saved = self
155                .owner_contribution_repository
156                .create(&contribution)
157                .await?;
158
159            contributions.push(saved);
160        }
161
162        Ok(contributions)
163    }
164
165    /// Cancel a call for funds
166    pub async fn cancel_call_for_funds(&self, id: Uuid) -> Result<CallForFunds, String> {
167        let mut call_for_funds = self
168            .call_for_funds_repository
169            .find_by_id(id)
170            .await?
171            .ok_or_else(|| "Call for funds not found".to_string())?;
172
173        call_for_funds.cancel();
174
175        self.call_for_funds_repository.update(&call_for_funds).await
176    }
177
178    /// Get all overdue calls for funds
179    pub async fn get_overdue_calls(&self) -> Result<Vec<CallForFunds>, String> {
180        self.call_for_funds_repository.find_overdue().await
181    }
182
183    /// Delete a call for funds (only if not sent)
184    pub async fn delete_call_for_funds(&self, id: Uuid) -> Result<bool, String> {
185        let call_for_funds = self
186            .call_for_funds_repository
187            .find_by_id(id)
188            .await?
189            .ok_or_else(|| "Call for funds not found".to_string())?;
190
191        // Don't allow deletion if already sent
192        if call_for_funds.status != crate::domain::entities::CallForFundsStatus::Draft {
193            return Err("Cannot delete a call for funds that has been sent".to_string());
194        }
195
196        self.call_for_funds_repository.delete(id).await
197    }
198}