koprogo_api/application/use_cases/
budget_use_cases.rs

1use crate::application::dto::{
2    BudgetResponse, CreateBudgetRequest, PageRequest, UpdateBudgetRequest,
3};
4use crate::application::ports::{
5    BudgetRepository, BudgetStatsResponse, BudgetVarianceResponse, BuildingRepository,
6    ExpenseRepository,
7};
8use crate::domain::entities::{Budget, BudgetStatus};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct BudgetUseCases {
13    repository: Arc<dyn BudgetRepository>,
14    building_repository: Arc<dyn BuildingRepository>,
15    #[allow(dead_code)]
16    expense_repository: Arc<dyn ExpenseRepository>,
17}
18
19impl BudgetUseCases {
20    pub fn new(
21        repository: Arc<dyn BudgetRepository>,
22        building_repository: Arc<dyn BuildingRepository>,
23        expense_repository: Arc<dyn ExpenseRepository>,
24    ) -> Self {
25        Self {
26            repository,
27            building_repository,
28            expense_repository,
29        }
30    }
31
32    /// Create a new budget
33    pub async fn create_budget(
34        &self,
35        request: CreateBudgetRequest,
36    ) -> Result<BudgetResponse, String> {
37        // Verify building exists
38        let _building = self
39            .building_repository
40            .find_by_id(request.building_id)
41            .await?
42            .ok_or_else(|| "Building not found".to_string())?;
43
44        // Check if budget already exists for this building/fiscal_year
45        if let Some(_existing) = self
46            .repository
47            .find_by_building_and_fiscal_year(request.building_id, request.fiscal_year)
48            .await?
49        {
50            return Err(format!(
51                "Budget already exists for building {} and fiscal year {}",
52                request.building_id, request.fiscal_year
53            ));
54        }
55
56        // Create budget
57        let mut budget = Budget::new(
58            request.organization_id,
59            request.building_id,
60            request.fiscal_year,
61            request.ordinary_budget,
62            request.extraordinary_budget,
63        )?;
64
65        // Set notes if provided
66        if let Some(notes) = request.notes {
67            budget.update_notes(notes);
68        }
69
70        let created = self.repository.create(&budget).await?;
71        Ok(BudgetResponse::from(created))
72    }
73
74    /// Get budget by ID
75    pub async fn get_budget(&self, id: Uuid) -> Result<Option<BudgetResponse>, String> {
76        let budget = self.repository.find_by_id(id).await?;
77        Ok(budget.map(BudgetResponse::from))
78    }
79
80    /// Get budget for a building and fiscal year
81    pub async fn get_by_building_and_fiscal_year(
82        &self,
83        building_id: Uuid,
84        fiscal_year: i32,
85    ) -> Result<Option<BudgetResponse>, String> {
86        let budget = self
87            .repository
88            .find_by_building_and_fiscal_year(building_id, fiscal_year)
89            .await?;
90        Ok(budget.map(BudgetResponse::from))
91    }
92
93    /// Get active budget for a building
94    pub async fn get_active_budget(
95        &self,
96        building_id: Uuid,
97    ) -> Result<Option<BudgetResponse>, String> {
98        let budget = self.repository.find_active_by_building(building_id).await?;
99        Ok(budget.map(BudgetResponse::from))
100    }
101
102    /// List budgets for a building
103    pub async fn list_by_building(&self, building_id: Uuid) -> Result<Vec<BudgetResponse>, String> {
104        let budgets = self.repository.find_by_building(building_id).await?;
105        Ok(budgets.into_iter().map(BudgetResponse::from).collect())
106    }
107
108    /// List budgets by fiscal year
109    pub async fn list_by_fiscal_year(
110        &self,
111        organization_id: Uuid,
112        fiscal_year: i32,
113    ) -> Result<Vec<BudgetResponse>, String> {
114        let budgets = self
115            .repository
116            .find_by_fiscal_year(organization_id, fiscal_year)
117            .await?;
118        Ok(budgets.into_iter().map(BudgetResponse::from).collect())
119    }
120
121    /// List budgets by status
122    pub async fn list_by_status(
123        &self,
124        organization_id: Uuid,
125        status: BudgetStatus,
126    ) -> Result<Vec<BudgetResponse>, String> {
127        let budgets = self
128            .repository
129            .find_by_status(organization_id, status)
130            .await?;
131        Ok(budgets.into_iter().map(BudgetResponse::from).collect())
132    }
133
134    /// List budgets paginated
135    pub async fn list_paginated(
136        &self,
137        page_request: &PageRequest,
138        organization_id: Option<Uuid>,
139        building_id: Option<Uuid>,
140        status: Option<BudgetStatus>,
141    ) -> Result<(Vec<BudgetResponse>, i64), String> {
142        let (budgets, total) = self
143            .repository
144            .find_all_paginated(page_request, organization_id, building_id, status)
145            .await?;
146
147        let dtos = budgets.into_iter().map(BudgetResponse::from).collect();
148        Ok((dtos, total))
149    }
150
151    /// Update budget amounts (Draft only)
152    pub async fn update_budget(
153        &self,
154        id: Uuid,
155        request: UpdateBudgetRequest,
156    ) -> Result<BudgetResponse, String> {
157        let mut budget = self
158            .repository
159            .find_by_id(id)
160            .await?
161            .ok_or_else(|| "Budget not found".to_string())?;
162
163        // Apply updates
164        if let Some(ordinary) = request.ordinary_budget {
165            if let Some(extraordinary) = request.extraordinary_budget {
166                budget.update_amounts(ordinary, extraordinary)?;
167            }
168        }
169
170        if let Some(notes) = request.notes {
171            budget.update_notes(notes);
172        }
173
174        let updated = self.repository.update(&budget).await?;
175        Ok(BudgetResponse::from(updated))
176    }
177
178    /// Submit budget for approval
179    pub async fn submit_for_approval(&self, id: Uuid) -> Result<BudgetResponse, String> {
180        let mut budget = self
181            .repository
182            .find_by_id(id)
183            .await?
184            .ok_or_else(|| "Budget not found".to_string())?;
185
186        budget.submit_for_approval()?;
187
188        let updated = self.repository.update(&budget).await?;
189        Ok(BudgetResponse::from(updated))
190    }
191
192    /// Approve budget (requires meeting_id for legal traceability)
193    pub async fn approve_budget(
194        &self,
195        id: Uuid,
196        meeting_id: Uuid,
197    ) -> Result<BudgetResponse, String> {
198        let mut budget = self
199            .repository
200            .find_by_id(id)
201            .await?
202            .ok_or_else(|| "Budget not found".to_string())?;
203
204        budget.approve(meeting_id)?;
205
206        let updated = self.repository.update(&budget).await?;
207        Ok(BudgetResponse::from(updated))
208    }
209
210    /// Reject budget (with optional reason)
211    pub async fn reject_budget(
212        &self,
213        id: Uuid,
214        reason: Option<String>,
215    ) -> Result<BudgetResponse, String> {
216        let mut budget = self
217            .repository
218            .find_by_id(id)
219            .await?
220            .ok_or_else(|| "Budget not found".to_string())?;
221
222        // Add rejection reason to notes
223        if let Some(reason) = reason {
224            let current_notes = budget.notes.clone().unwrap_or_default();
225            let new_notes = if current_notes.is_empty() {
226                format!("REJECTED: {}", reason)
227            } else {
228                format!("{}\n\nREJECTED: {}", current_notes, reason)
229            };
230            budget.update_notes(new_notes);
231        }
232
233        budget.reject()?;
234
235        let updated = self.repository.update(&budget).await?;
236        Ok(BudgetResponse::from(updated))
237    }
238
239    /// Archive budget
240    pub async fn archive_budget(&self, id: Uuid) -> Result<BudgetResponse, String> {
241        let mut budget = self
242            .repository
243            .find_by_id(id)
244            .await?
245            .ok_or_else(|| "Budget not found".to_string())?;
246
247        budget.archive()?;
248
249        let updated = self.repository.update(&budget).await?;
250        Ok(BudgetResponse::from(updated))
251    }
252
253    /// Delete budget
254    pub async fn delete_budget(&self, id: Uuid) -> Result<bool, String> {
255        self.repository.delete(id).await
256    }
257
258    /// Get budget statistics
259    pub async fn get_stats(&self, organization_id: Uuid) -> Result<BudgetStatsResponse, String> {
260        self.repository.get_stats(organization_id).await
261    }
262
263    /// Get budget variance analysis (budget vs actual expenses)
264    pub async fn get_variance(
265        &self,
266        budget_id: Uuid,
267    ) -> Result<Option<BudgetVarianceResponse>, String> {
268        self.repository.get_variance(budget_id).await
269    }
270}