use crate::types::{AccountId, CategoryId, MerchantId, TagId, TransactionId};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Account {
pub id: AccountId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub balance: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<iso_currency::Currency>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
pub account_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Category {
pub id: CategoryId,
pub name: String,
pub color: String,
pub icon: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Merchant {
pub id: MerchantId,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Tag {
pub id: TagId,
pub name: String,
pub color: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Transfer {
pub id: TransactionId,
pub amount: String,
pub currency: iso_currency::Currency,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub other_account: Option<Account>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransactionNature {
#[serde(alias = "inflow")]
Income,
#[serde(alias = "outflow")]
Expense,
}
impl std::fmt::Display for TransactionNature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Income => write!(f, "income"),
Self::Expense => write!(f, "expense"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseTransactionNatureError(String);
impl std::fmt::Display for ParseTransactionNatureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid transaction nature: {}", self.0)
}
}
impl std::error::Error for ParseTransactionNatureError {}
impl std::str::FromStr for TransactionNature {
type Err = ParseTransactionNatureError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"income" | "inflow" => Ok(Self::Income),
"expense" | "outflow" => Ok(Self::Expense),
_ => Err(ParseTransactionNatureError(s.to_string())),
}
}
}
impl TryFrom<&str> for TransactionNature {
type Error = ParseTransactionNatureError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<String> for TransactionNature {
type Error = ParseTransactionNatureError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransactionType {
Income,
Expense,
}
impl std::fmt::Display for TransactionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Income => "income",
Self::Expense => "expense",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseTransactionTypeError(String);
impl std::fmt::Display for ParseTransactionTypeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid transaction type: {}", self.0)
}
}
impl std::error::Error for ParseTransactionTypeError {}
impl std::str::FromStr for TransactionType {
type Err = ParseTransactionTypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"income" => Ok(Self::Income),
"expense" => Ok(Self::Expense),
_ => Err(ParseTransactionTypeError(s.to_string())),
}
}
}
impl TryFrom<&str> for TransactionType {
type Error = ParseTransactionTypeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<String> for TransactionType {
type Error = ParseTransactionTypeError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct Transaction {
pub id: TransactionId,
#[serde(with = "crate::serde::naive_date")]
pub date: DateTime<Utc>,
pub amount: String,
pub amount_cents: i64,
pub signed_amount_cents: i64,
pub currency: iso_currency::Currency,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
pub classification: String,
pub account: Account,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<Category>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merchant: Option<Merchant>,
pub tags: Vec<Tag>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transfer: Option<Transfer>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub struct TransactionCollection {
pub transactions: Vec<Transaction>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub(crate) struct CreateTransactionRequest {
pub transaction: CreateTransactionData,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub(crate) struct CreateTransactionData {
pub account_id: AccountId,
pub date: DateTime<Utc>,
pub amount: Decimal,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<iso_currency::Currency>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category_id: Option<CategoryId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merchant_id: Option<MerchantId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nature: Option<TransactionNature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag_ids: Option<Vec<TagId>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub(crate) struct UpdateTransactionRequest {
pub transaction: UpdateTransactionData,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
pub(crate) struct UpdateTransactionData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amount: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<iso_currency::Currency>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category_id: Option<CategoryId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merchant_id: Option<MerchantId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nature: Option<TransactionNature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag_ids: Option<Vec<TagId>>,
}