koprogo_api/application/use_cases/
budget_use_cases.rs1use 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 pub async fn create_budget(
34 &self,
35 request: CreateBudgetRequest,
36 ) -> Result<BudgetResponse, String> {
37 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn delete_budget(&self, id: Uuid) -> Result<bool, String> {
255 self.repository.delete(id).await
256 }
257
258 pub async fn get_stats(&self, organization_id: Uuid) -> Result<BudgetStatsResponse, String> {
260 self.repository.get_stats(organization_id).await
261 }
262
263 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}