koprogo_api/application/dto/
dashboard_dto.rs1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Serialize, Deserialize)]
15pub struct AccountantDashboardStats {
16 pub total_expenses_current_month: Decimal,
18
19 pub total_paid: Decimal,
21
22 #[serde(with = "rust_decimal::serde::float")]
24 pub paid_percentage: Decimal,
25
26 pub total_pending: Decimal,
28
29 #[serde(with = "rust_decimal::serde::float")]
31 pub pending_percentage: Decimal,
32
33 pub owners_with_overdue: i64,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum TransactionType {
48 PaymentReceived,
50 PaymentMade,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
56pub struct RecentTransaction {
57 pub id: Uuid,
59
60 pub transaction_type: TransactionType,
62
63 pub description: String,
65
66 pub related_entity: Option<String>,
68
69 pub amount: Decimal,
71
72 pub date: DateTime<Utc>,
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use rust_decimal_macros::dec;
80 use serde_json::Value;
81
82 fn sample(paid_pct: Decimal, pending_pct: Decimal) -> AccountantDashboardStats {
83 AccountantDashboardStats {
84 total_expenses_current_month: dec!(1000.00),
85 total_paid: dec!(425.50),
86 paid_percentage: paid_pct,
87 total_pending: dec!(574.50),
88 pending_percentage: pending_pct,
89 owners_with_overdue: 3,
90 }
91 }
92
93 #[test]
95 fn paid_and_pending_percentage_serialize_as_json_number() {
96 let json: Value = serde_json::to_value(sample(dec!(42.5), dec!(57.5))).unwrap();
97 assert!(
98 json["paid_percentage"].is_number(),
99 "paid_percentage must be JSON number, got {:?}",
100 json["paid_percentage"]
101 );
102 assert!(
103 json["pending_percentage"].is_number(),
104 "pending_percentage must be JSON number, got {:?}",
105 json["pending_percentage"]
106 );
107 assert_eq!(json["paid_percentage"].as_f64().unwrap(), 42.5);
108 }
109
110 #[test]
112 fn percentage_zero_and_hundred_serialize_as_number() {
113 let json: Value = serde_json::to_value(sample(dec!(0), dec!(100))).unwrap();
114 assert_eq!(json["paid_percentage"].as_f64().unwrap(), 0.0);
115 assert_eq!(json["pending_percentage"].as_f64().unwrap(), 100.0);
116 }
117
118 #[test]
120 fn monetary_fields_remain_json_strings() {
121 let json: Value = serde_json::to_value(sample(dec!(50), dec!(50))).unwrap();
122 for field in [
123 "total_expenses_current_month",
124 "total_paid",
125 "total_pending",
126 ] {
127 assert!(
128 json[field].is_string(),
129 "{} must stay JSON string (Decimal exact), got {:?}",
130 field,
131 json[field]
132 );
133 }
134 }
135
136 #[test]
138 fn percentage_roundtrip_preserves_value() {
139 let original = sample(dec!(33.33), dec!(66.67));
140 let json = serde_json::to_string(&original).unwrap();
141 let parsed: AccountantDashboardStats = serde_json::from_str(&json).unwrap();
142 assert_eq!(parsed.paid_percentage, dec!(33.33));
143 assert_eq!(parsed.pending_percentage, dec!(66.67));
144 }
145}