From 26d0be20c276bf5fc50f4ac08634309f8a718611 Mon Sep 17 00:00:00 2001 From: lashman Date: Sun, 1 Mar 2026 23:56:41 +0200 Subject: [PATCH] Add core data models for transactions, categories, budgets --- outlay-core/src/models.rs | 210 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/outlay-core/src/models.rs b/outlay-core/src/models.rs index e69de29..e54223f 100644 --- a/outlay-core/src/models.rs +++ b/outlay-core/src/models.rs @@ -0,0 +1,210 @@ +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TransactionType { + Expense, + Income, +} + +impl TransactionType { + pub fn as_str(&self) -> &'static str { + match self { + TransactionType::Expense => "expense", + TransactionType::Income => "income", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "expense" => Some(TransactionType::Expense), + "income" => Some(TransactionType::Income), + _ => None, + } + } +} + +impl fmt::Display for TransactionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Frequency { + Daily, + Weekly, + Biweekly, + Monthly, + Yearly, +} + +impl Frequency { + pub fn as_str(&self) -> &'static str { + match self { + Frequency::Daily => "daily", + Frequency::Weekly => "weekly", + Frequency::Biweekly => "biweekly", + Frequency::Monthly => "monthly", + Frequency::Yearly => "yearly", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "daily" => Some(Frequency::Daily), + "weekly" => Some(Frequency::Weekly), + "biweekly" => Some(Frequency::Biweekly), + "monthly" => Some(Frequency::Monthly), + "yearly" => Some(Frequency::Yearly), + _ => None, + } + } +} + +impl fmt::Display for Frequency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Category { + pub id: i64, + pub name: String, + pub icon: Option, + pub color: Option, + pub transaction_type: TransactionType, + pub is_default: bool, + pub sort_order: i32, +} + +#[derive(Debug, Clone)] +pub struct NewCategory { + pub name: String, + pub icon: Option, + pub color: Option, + pub transaction_type: TransactionType, + pub sort_order: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + pub id: i64, + pub amount: f64, + pub transaction_type: TransactionType, + pub category_id: i64, + pub currency: String, + pub exchange_rate: f64, + pub note: Option, + pub date: NaiveDate, + pub created_at: String, + pub recurring_id: Option, +} + +#[derive(Debug, Clone)] +pub struct NewTransaction { + pub amount: f64, + pub transaction_type: TransactionType, + pub category_id: i64, + pub currency: String, + pub exchange_rate: f64, + pub note: Option, + pub date: NaiveDate, + pub recurring_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Budget { + pub id: i64, + pub category_id: i64, + pub amount: f64, + pub month: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecurringTransaction { + pub id: i64, + pub amount: f64, + pub transaction_type: TransactionType, + pub category_id: i64, + pub currency: String, + pub note: Option, + pub frequency: Frequency, + pub start_date: NaiveDate, + pub end_date: Option, + pub last_generated: Option, + pub active: bool, +} + +#[derive(Debug, Clone)] +pub struct NewRecurringTransaction { + pub amount: f64, + pub transaction_type: TransactionType, + pub category_id: i64, + pub currency: String, + pub note: Option, + pub frequency: Frequency, + pub start_date: NaiveDate, + pub end_date: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRate { + pub base: String, + pub target: String, + pub rate: f64, + pub fetched_at: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transaction_type_round_trip() { + for tt in [TransactionType::Expense, TransactionType::Income] { + let s = tt.as_str(); + let parsed = TransactionType::from_str(s).unwrap(); + assert_eq!(tt, parsed); + } + } + + #[test] + fn test_transaction_type_invalid() { + assert_eq!(TransactionType::from_str("bogus"), None); + } + + #[test] + fn test_frequency_round_trip() { + for f in [ + Frequency::Daily, + Frequency::Weekly, + Frequency::Biweekly, + Frequency::Monthly, + Frequency::Yearly, + ] { + let s = f.as_str(); + let parsed = Frequency::from_str(s).unwrap(); + assert_eq!(f, parsed); + } + } + + #[test] + fn test_frequency_invalid() { + assert_eq!(Frequency::from_str("quarterly"), None); + } + + #[test] + fn test_transaction_type_display() { + assert_eq!(format!("{}", TransactionType::Expense), "expense"); + assert_eq!(format!("{}", TransactionType::Income), "income"); + } + + #[test] + fn test_frequency_display() { + assert_eq!(format!("{}", Frequency::Monthly), "monthly"); + assert_eq!(format!("{}", Frequency::Biweekly), "biweekly"); + } +}