use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use super::{TagId, UserId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Budget {
#[serde(with = "chrono::serde::ts_seconds")]
pub changed: DateTime<Utc>,
pub user: UserId,
pub tag: Option<TagId>,
pub date: NaiveDate,
pub income: f64,
pub income_lock: bool,
pub outcome: f64,
pub outcome_lock: bool,
#[serde(default)]
pub is_income_forecast: Option<bool>,
#[serde(default)]
pub is_outcome_forecast: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_budget() {
let json = r#"{
"changed": 1700000000,
"user": 123,
"tag": "tag-groceries",
"date": "2024-01-01",
"income": 0,
"incomeLock": false,
"outcome": 30000.0,
"outcomeLock": true
}"#;
let budget: Budget = serde_json::from_str(json).unwrap();
assert_eq!(budget.tag, Some(TagId::new("tag-groceries".to_owned())));
assert!((budget.outcome - 30_000.0).abs() < f64::EPSILON);
assert!(budget.outcome_lock);
assert!(!budget.income_lock);
}
#[test]
fn deserialize_aggregate_budget() {
let json = r#"{
"changed": 1700000000,
"user": 123,
"tag": null,
"date": "2024-01-01",
"income": 100000.0,
"incomeLock": true,
"outcome": 80000.0,
"outcomeLock": true
}"#;
let budget: Budget = serde_json::from_str(json).unwrap();
assert!(budget.tag.is_none());
}
#[test]
fn serialize_roundtrip() {
let budget = Budget {
changed: DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
user: UserId::new(1),
tag: Some(TagId::new("t-1".to_owned())),
date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
income: 0.0,
income_lock: false,
outcome: 5000.0,
outcome_lock: false,
is_income_forecast: None,
is_outcome_forecast: None,
};
let json = serde_json::to_string(&budget).unwrap();
let deserialized: Budget = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, budget);
}
}