Skip to main content

koprogo_api/application/ports/
budget_repository.rs

1use crate::application::dto::PageRequest;
2use crate::application::error::AppError;
3use crate::domain::entities::{Budget, BudgetStatus};
4use async_trait::async_trait;
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// Repository trait for Budget persistence
10#[async_trait]
11pub trait BudgetRepository: Send + Sync {
12    /// Create a new budget
13    async fn create(&self, budget: &Budget) -> Result<Budget, AppError>;
14
15    /// Find budget by ID
16    async fn find_by_id(&self, id: Uuid) -> Result<Option<Budget>, AppError>;
17
18    /// Find budget by building and fiscal year (should be unique)
19    async fn find_by_building_and_fiscal_year(
20        &self,
21        building_id: Uuid,
22        fiscal_year: i32,
23    ) -> Result<Option<Budget>, AppError>;
24
25    /// Find all budgets for a building
26    async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Budget>, AppError>;
27
28    /// Find active budget for a building (status = Approved, most recent fiscal year)
29    async fn find_active_by_building(&self, building_id: Uuid) -> Result<Option<Budget>, AppError>;
30
31    /// Find budgets by fiscal year across all buildings in organization
32    async fn find_by_fiscal_year(
33        &self,
34        organization_id: Uuid,
35        fiscal_year: i32,
36    ) -> Result<Vec<Budget>, AppError>;
37
38    /// Find budgets by status
39    async fn find_by_status(
40        &self,
41        organization_id: Uuid,
42        status: BudgetStatus,
43    ) -> Result<Vec<Budget>, AppError>;
44
45    /// Find all budgets paginated
46    async fn find_all_paginated(
47        &self,
48        page_request: &PageRequest,
49        organization_id: Option<Uuid>,
50        building_id: Option<Uuid>,
51        status: Option<BudgetStatus>,
52    ) -> Result<(Vec<Budget>, i64), AppError>;
53
54    /// Update existing budget
55    async fn update(&self, budget: &Budget) -> Result<Budget, AppError>;
56
57    /// Delete budget by ID
58    async fn delete(&self, id: Uuid) -> Result<bool, AppError>;
59
60    /// Get budget statistics for dashboard
61    async fn get_stats(&self, organization_id: Uuid) -> Result<BudgetStatsResponse, AppError>;
62
63    /// Get budget variance analysis (budget vs actual expenses)
64    async fn get_variance(
65        &self,
66        budget_id: Uuid,
67    ) -> Result<Option<BudgetVarianceResponse>, AppError>;
68}
69
70/// Statistics response for budgets
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct BudgetStatsResponse {
73    pub total_budgets: i64,
74    pub draft_count: i64,
75    pub submitted_count: i64,
76    pub approved_count: i64,
77    pub rejected_count: i64,
78    pub archived_count: i64,
79    // #661 — moyennes de montants : Decimal comme le reste du PCMN.
80    #[serde(with = "rust_decimal::serde::float")]
81    pub average_total_budget: Decimal,
82    #[serde(with = "rust_decimal::serde::float")]
83    pub average_monthly_provision: Decimal,
84}
85
86/// Variance analysis response
87///
88/// Issue #661 — tous les montants sont en `Decimal` : ce sont des charges de
89/// copropriété (PCMN), et l'ADR-0008 §A n'accorde aucun carve-out `f64` à un
90/// montant. Les `*_pct` suivent, parce qu'ils alimentent le seuil métier
91/// `has_overruns` (dépassement > 10%) — un pourcentage comparé à un seuil
92/// n'est pas un pourcentage d'affichage.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct BudgetVarianceResponse {
95    pub budget_id: Uuid,
96    pub fiscal_year: i32,
97    pub building_id: Uuid,
98    #[serde(with = "rust_decimal::serde::float")]
99    pub budgeted_ordinary: Decimal,
100    #[serde(with = "rust_decimal::serde::float")]
101    pub budgeted_extraordinary: Decimal,
102    #[serde(with = "rust_decimal::serde::float")]
103    pub budgeted_total: Decimal,
104    #[serde(with = "rust_decimal::serde::float")]
105    pub actual_ordinary: Decimal,
106    #[serde(with = "rust_decimal::serde::float")]
107    pub actual_extraordinary: Decimal,
108    #[serde(with = "rust_decimal::serde::float")]
109    pub actual_total: Decimal,
110    #[serde(with = "rust_decimal::serde::float")]
111    pub variance_ordinary: Decimal,
112    #[serde(with = "rust_decimal::serde::float")]
113    pub variance_extraordinary: Decimal,
114    #[serde(with = "rust_decimal::serde::float")]
115    pub variance_total: Decimal,
116    #[serde(with = "rust_decimal::serde::float")]
117    pub variance_ordinary_pct: Decimal,
118    #[serde(with = "rust_decimal::serde::float")]
119    pub variance_extraordinary_pct: Decimal,
120    #[serde(with = "rust_decimal::serde::float")]
121    pub variance_total_pct: Decimal,
122    pub has_overruns: bool,
123    pub overrun_categories: Vec<String>,
124    pub months_elapsed: i32,
125    #[serde(with = "rust_decimal::serde::float")]
126    pub projected_year_end_total: Decimal,
127}