//! Time helpers and the `MonthYear` type used for card expiries. use serde::{Deserialize, Serialize}; /// Current Unix timestamp in seconds. pub fn now_unix() -> i64 { chrono::Utc::now().timestamp() } /// Month + year (1-12 / e.g. 2026). Used for card expiries. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct MonthYear { pub month: u8, pub year: u16, } impl MonthYear { pub fn new(month: u8, year: u16) -> Result { if !(1..=12).contains(&month) { return Err("month must be 1..=12"); } if year < 2000 || year > 2099 { return Err("year must be 2000..=2099"); } Ok(Self { month, year }) } } #[cfg(test)] mod tests { use super::*; #[test] fn now_unix_is_positive_and_recent() { let t = now_unix(); assert!(t > 1_700_000_000); // after late 2023 assert!(t < 4_000_000_000); // before 2096 } #[test] fn month_year_constructor_rejects_bad_month() { assert!(MonthYear::new(0, 2026).is_err()); assert!(MonthYear::new(13, 2026).is_err()); assert!(MonthYear::new(1, 2026).is_ok()); assert!(MonthYear::new(12, 2026).is_ok()); } #[test] fn month_year_constructor_rejects_bad_year() { assert!(MonthYear::new(1, 1999).is_err()); assert!(MonthYear::new(1, 2100).is_err()); assert!(MonthYear::new(1, 2000).is_ok()); assert!(MonthYear::new(1, 2099).is_ok()); } #[test] fn month_year_round_trips_through_json() { let my = MonthYear::new(7, 2030).unwrap(); let json = serde_json::to_string(&my).unwrap(); let parsed: MonthYear = serde_json::from_str(&json).unwrap(); assert_eq!(parsed, my); } }