koprogo_api/application/use_cases/
expense_use_cases.rs1use crate::application::dto::{
2 ApproveInvoiceDto, CreateExpenseDto, CreateInvoiceDraftDto, ExpenseFilters, ExpenseResponseDto,
3 InvoiceResponseDto, PageRequest, PendingInvoicesListDto, RejectInvoiceDto, SortOrder,
4 SubmitForApprovalDto, UpdateInvoiceDraftDto,
5};
6use crate::application::ports::ExpenseRepository;
7use crate::application::services::expense_accounting_service::ExpenseAccountingService;
8use crate::domain::entities::{ApprovalStatus, Expense};
9use chrono::DateTime;
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct ExpenseUseCases {
14 repository: Arc<dyn ExpenseRepository>,
15 accounting_service: Option<Arc<ExpenseAccountingService>>,
16}
17
18impl ExpenseUseCases {
19 pub fn new(repository: Arc<dyn ExpenseRepository>) -> Self {
20 Self {
21 repository,
22 accounting_service: None,
23 }
24 }
25
26 pub fn with_accounting_service(
27 repository: Arc<dyn ExpenseRepository>,
28 accounting_service: Arc<ExpenseAccountingService>,
29 ) -> Self {
30 Self {
31 repository,
32 accounting_service: Some(accounting_service),
33 }
34 }
35
36 pub async fn create_expense(
37 &self,
38 dto: CreateExpenseDto,
39 ) -> Result<ExpenseResponseDto, String> {
40 let organization_id = Uuid::parse_str(&dto.organization_id)
41 .map_err(|_| "Invalid organization_id format".to_string())?;
42 let building_id = Uuid::parse_str(&dto.building_id)
43 .map_err(|_| "Invalid building ID format".to_string())?;
44
45 let expense_date = DateTime::parse_from_rfc3339(&dto.expense_date)
46 .map_err(|_| "Invalid date format".to_string())?
47 .with_timezone(&chrono::Utc);
48
49 let expense = Expense::new(
50 organization_id,
51 building_id,
52 dto.category,
53 dto.description,
54 dto.amount,
55 expense_date,
56 dto.supplier,
57 dto.invoice_number,
58 dto.account_code,
59 )?;
60
61 let created = self.repository.create(&expense).await?;
62 Ok(self.to_response_dto(&created))
63 }
64
65 pub async fn get_expense(&self, id: Uuid) -> Result<Option<ExpenseResponseDto>, String> {
66 let expense = self.repository.find_by_id(id).await?;
67 Ok(expense.map(|e| self.to_response_dto(&e)))
68 }
69
70 pub async fn list_expenses_by_building(
71 &self,
72 building_id: Uuid,
73 ) -> Result<Vec<ExpenseResponseDto>, String> {
74 let expenses = self.repository.find_by_building(building_id).await?;
75 Ok(expenses.iter().map(|e| self.to_response_dto(e)).collect())
76 }
77
78 pub async fn list_expenses_paginated(
79 &self,
80 page_request: &PageRequest,
81 organization_id: Option<Uuid>,
82 ) -> Result<(Vec<ExpenseResponseDto>, i64), String> {
83 let filters = ExpenseFilters {
84 organization_id,
85 ..Default::default()
86 };
87
88 let (expenses, total) = self
89 .repository
90 .find_all_paginated(page_request, &filters)
91 .await?;
92
93 let dtos = expenses.iter().map(|e| self.to_response_dto(e)).collect();
94 Ok((dtos, total))
95 }
96
97 pub async fn mark_as_paid(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
101 let mut expense = self
102 .repository
103 .find_by_id(id)
104 .await?
105 .ok_or_else(|| "Expense not found".to_string())?;
106
107 expense.mark_as_paid()?;
108
109 let updated = self.repository.update(&expense).await?;
110
111 if let Some(ref accounting_service) = self.accounting_service {
113 if let Err(e) = accounting_service
114 .generate_payment_entry(&updated, None, None)
115 .await
116 {
117 log::warn!(
118 "Failed to generate payment journal entry for expense {}: {}",
119 updated.id,
120 e
121 );
122 }
125 }
126
127 Ok(self.to_response_dto(&updated))
128 }
129
130 pub async fn mark_as_overdue(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
131 let mut expense = self
132 .repository
133 .find_by_id(id)
134 .await?
135 .ok_or_else(|| "Expense not found".to_string())?;
136
137 expense.mark_as_overdue()?;
138
139 let updated = self.repository.update(&expense).await?;
140 Ok(self.to_response_dto(&updated))
141 }
142
143 pub async fn cancel_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
144 let mut expense = self
145 .repository
146 .find_by_id(id)
147 .await?
148 .ok_or_else(|| "Expense not found".to_string())?;
149
150 expense.cancel()?;
151
152 let updated = self.repository.update(&expense).await?;
153 Ok(self.to_response_dto(&updated))
154 }
155
156 pub async fn reactivate_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
157 let mut expense = self
158 .repository
159 .find_by_id(id)
160 .await?
161 .ok_or_else(|| "Expense not found".to_string())?;
162
163 expense.reactivate()?;
164
165 let updated = self.repository.update(&expense).await?;
166 Ok(self.to_response_dto(&updated))
167 }
168
169 pub async fn unpay_expense(&self, id: Uuid) -> Result<ExpenseResponseDto, String> {
170 let mut expense = self
171 .repository
172 .find_by_id(id)
173 .await?
174 .ok_or_else(|| "Expense not found".to_string())?;
175
176 expense.unpay()?;
177
178 let updated = self.repository.update(&expense).await?;
179 Ok(self.to_response_dto(&updated))
180 }
181
182 pub async fn create_invoice_draft(
186 &self,
187 dto: CreateInvoiceDraftDto,
188 ) -> Result<InvoiceResponseDto, String> {
189 let organization_id = Uuid::parse_str(&dto.organization_id)
190 .map_err(|_| "Invalid organization_id format".to_string())?;
191 let building_id = Uuid::parse_str(&dto.building_id)
192 .map_err(|_| "Invalid building ID format".to_string())?;
193
194 let invoice_date = DateTime::parse_from_rfc3339(&dto.invoice_date)
195 .map_err(|_| "Invalid invoice_date format".to_string())?
196 .with_timezone(&chrono::Utc);
197
198 let due_date = dto
199 .due_date
200 .map(|d| {
201 DateTime::parse_from_rfc3339(&d)
202 .map_err(|_| "Invalid due_date format".to_string())
203 .map(|dt| dt.with_timezone(&chrono::Utc))
204 })
205 .transpose()?;
206
207 let invoice = Expense::new_with_vat(
208 organization_id,
209 building_id,
210 dto.category,
211 dto.description,
212 dto.amount_excl_vat,
213 dto.vat_rate,
214 invoice_date,
215 due_date,
216 dto.supplier,
217 dto.invoice_number,
218 None, )?;
220
221 let created = self.repository.create(&invoice).await?;
222 Ok(self.to_invoice_response_dto(&created))
223 }
224
225 pub async fn update_invoice_draft(
227 &self,
228 invoice_id: Uuid,
229 dto: UpdateInvoiceDraftDto,
230 ) -> Result<InvoiceResponseDto, String> {
231 let mut invoice = self
232 .repository
233 .find_by_id(invoice_id)
234 .await?
235 .ok_or_else(|| "Invoice not found".to_string())?;
236
237 if !invoice.can_be_modified() {
239 return Err(format!(
240 "Invoice cannot be modified (status: {:?})",
241 invoice.approval_status
242 ));
243 }
244
245 if let Some(desc) = dto.description {
247 invoice.description = desc;
248 }
249 if let Some(cat) = dto.category {
250 invoice.category = cat;
251 }
252 if let Some(amount_ht) = dto.amount_excl_vat {
253 invoice.amount_excl_vat = Some(amount_ht);
254 }
255 if let Some(vat_rate) = dto.vat_rate {
256 invoice.vat_rate = Some(vat_rate);
257 }
258
259 if dto.amount_excl_vat.is_some() || dto.vat_rate.is_some() {
261 invoice.recalculate_vat()?;
262 }
263
264 if let Some(inv_date) = dto.invoice_date {
265 let parsed_date = DateTime::parse_from_rfc3339(&inv_date)
266 .map_err(|_| "Invalid invoice_date format".to_string())?
267 .with_timezone(&chrono::Utc);
268 invoice.invoice_date = Some(parsed_date);
269 }
270
271 if let Some(due_date_str) = dto.due_date {
272 let parsed_date = DateTime::parse_from_rfc3339(&due_date_str)
273 .map_err(|_| "Invalid due_date format".to_string())?
274 .with_timezone(&chrono::Utc);
275 invoice.due_date = Some(parsed_date);
276 }
277
278 if dto.supplier.is_some() {
279 invoice.supplier = dto.supplier;
280 }
281 if dto.invoice_number.is_some() {
282 invoice.invoice_number = dto.invoice_number;
283 }
284
285 invoice.updated_at = chrono::Utc::now();
286
287 let updated = self.repository.update(&invoice).await?;
288 Ok(self.to_invoice_response_dto(&updated))
289 }
290
291 pub async fn submit_for_approval(
293 &self,
294 invoice_id: Uuid,
295 _dto: SubmitForApprovalDto,
296 ) -> Result<InvoiceResponseDto, String> {
297 let mut invoice = self
298 .repository
299 .find_by_id(invoice_id)
300 .await?
301 .ok_or_else(|| "Invoice not found".to_string())?;
302
303 invoice.submit_for_approval()?;
304
305 let updated = self.repository.update(&invoice).await?;
306 Ok(self.to_invoice_response_dto(&updated))
307 }
308
309 pub async fn approve_invoice(
313 &self,
314 invoice_id: Uuid,
315 dto: ApproveInvoiceDto,
316 ) -> Result<InvoiceResponseDto, String> {
317 let mut invoice = self
318 .repository
319 .find_by_id(invoice_id)
320 .await?
321 .ok_or_else(|| "Invoice not found".to_string())?;
322
323 let approved_by_user_id = Uuid::parse_str(&dto.approved_by_user_id)
324 .map_err(|_| "Invalid approved_by_user_id format".to_string())?;
325
326 invoice.approve(approved_by_user_id)?;
327
328 let updated = self.repository.update(&invoice).await?;
329
330 if let Some(ref accounting_service) = self.accounting_service {
332 if let Err(e) = accounting_service
333 .generate_journal_entry_for_expense(&updated, Some(approved_by_user_id))
334 .await
335 {
336 log::warn!(
337 "Failed to generate journal entry for approved expense {}: {}",
338 updated.id,
339 e
340 );
341 }
344 }
345
346 Ok(self.to_invoice_response_dto(&updated))
347 }
348
349 pub async fn reject_invoice(
351 &self,
352 invoice_id: Uuid,
353 dto: RejectInvoiceDto,
354 ) -> Result<InvoiceResponseDto, String> {
355 let mut invoice = self
356 .repository
357 .find_by_id(invoice_id)
358 .await?
359 .ok_or_else(|| "Invoice not found".to_string())?;
360
361 let rejected_by_user_id = Uuid::parse_str(&dto.rejected_by_user_id)
362 .map_err(|_| "Invalid rejected_by_user_id format".to_string())?;
363
364 invoice.reject(rejected_by_user_id, dto.rejection_reason)?;
365
366 let updated = self.repository.update(&invoice).await?;
367 Ok(self.to_invoice_response_dto(&updated))
368 }
369
370 pub async fn get_pending_invoices(
372 &self,
373 organization_id: Uuid,
374 ) -> Result<PendingInvoicesListDto, String> {
375 let filters = ExpenseFilters {
376 organization_id: Some(organization_id),
377 approval_status: Some(ApprovalStatus::PendingApproval),
378 ..Default::default()
379 };
380
381 let page_request = PageRequest {
383 page: 1,
384 per_page: 1000, sort_by: None,
386 order: SortOrder::default(),
387 };
388
389 let (expenses, _total) = self
390 .repository
391 .find_all_paginated(&page_request, &filters)
392 .await?;
393
394 let invoices: Vec<InvoiceResponseDto> = expenses
395 .iter()
396 .map(|e| self.to_invoice_response_dto(e))
397 .collect();
398
399 Ok(PendingInvoicesListDto {
400 count: invoices.len(),
401 invoices,
402 })
403 }
404
405 pub async fn get_invoice(&self, id: Uuid) -> Result<Option<InvoiceResponseDto>, String> {
407 let expense = self.repository.find_by_id(id).await?;
408 Ok(expense.map(|e| self.to_invoice_response_dto(&e)))
409 }
410
411 fn to_response_dto(&self, expense: &Expense) -> ExpenseResponseDto {
414 ExpenseResponseDto {
415 id: expense.id.to_string(),
416 building_id: expense.building_id.to_string(),
417 category: expense.category.clone(),
418 description: expense.description.clone(),
419 amount: expense.amount,
420 expense_date: expense.expense_date.to_rfc3339(),
421 payment_status: expense.payment_status.clone(),
422 approval_status: expense.approval_status.clone(),
423 supplier: expense.supplier.clone(),
424 invoice_number: expense.invoice_number.clone(),
425 account_code: expense.account_code.clone(),
426 }
427 }
428
429 fn to_invoice_response_dto(&self, expense: &Expense) -> InvoiceResponseDto {
430 InvoiceResponseDto {
431 id: expense.id.to_string(),
432 organization_id: expense.organization_id.to_string(),
433 building_id: expense.building_id.to_string(),
434 category: expense.category.clone(),
435 description: expense.description.clone(),
436
437 amount: expense.amount,
439 amount_excl_vat: expense.amount_excl_vat,
440 vat_rate: expense.vat_rate,
441 vat_amount: expense.vat_amount,
442 amount_incl_vat: expense.amount_incl_vat,
443
444 expense_date: expense.expense_date.to_rfc3339(),
446 invoice_date: expense.invoice_date.map(|d| d.to_rfc3339()),
447 due_date: expense.due_date.map(|d| d.to_rfc3339()),
448 paid_date: expense.paid_date.map(|d| d.to_rfc3339()),
449
450 approval_status: expense.approval_status.clone(),
452 submitted_at: expense.submitted_at.map(|d| d.to_rfc3339()),
453 approved_by: expense.approved_by.map(|u| u.to_string()),
454 approved_at: expense.approved_at.map(|d| d.to_rfc3339()),
455 rejection_reason: expense.rejection_reason.clone(),
456
457 payment_status: expense.payment_status.clone(),
459 supplier: expense.supplier.clone(),
460 invoice_number: expense.invoice_number.clone(),
461
462 created_at: expense.created_at.to_rfc3339(),
463 updated_at: expense.updated_at.to_rfc3339(),
464 }
465 }
466}