use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use super::{AccountId, InstrumentId, MerchantId, ReminderMarkerId, TagId, TransactionId, UserId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub id: TransactionId,
#[serde(with = "chrono::serde::ts_seconds")]
pub changed: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_seconds")]
pub created: DateTime<Utc>,
pub user: UserId,
pub deleted: bool,
pub hold: Option<bool>,
pub income_instrument: InstrumentId,
pub income_account: AccountId,
pub income: f64,
pub outcome_instrument: InstrumentId,
pub outcome_account: AccountId,
pub outcome: f64,
pub tag: Option<Vec<TagId>>,
pub merchant: Option<MerchantId>,
pub payee: Option<String>,
pub original_payee: Option<String>,
pub comment: Option<String>,
pub date: NaiveDate,
pub mcc: Option<i32>,
pub reminder_marker: Option<ReminderMarkerId>,
pub op_income: Option<f64>,
pub op_income_instrument: Option<InstrumentId>,
pub op_outcome: Option<f64>,
pub op_outcome_instrument: Option<InstrumentId>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
#[serde(default, rename = "incomeBankID")]
pub income_bank_id: Option<String>,
#[serde(default, rename = "outcomeBankID")]
pub outcome_bank_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub qr_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub viewed: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_simple_transaction() {
let json = r#"{
"id": "tx-001",
"changed": 1700000000,
"created": 1700000000,
"user": 123,
"deleted": false,
"hold": null,
"incomeInstrument": 1,
"incomeAccount": "acc-001",
"income": 0,
"outcomeInstrument": 1,
"outcomeAccount": "acc-001",
"outcome": 500.0,
"tag": ["tag-001"],
"merchant": "merchant-001",
"payee": "Coffee Shop",
"originalPayee": "COFFEE SHOP LLC",
"comment": "Morning coffee",
"date": "2024-01-15",
"mcc": 5812,
"reminderMarker": null,
"opIncome": null,
"opIncomeInstrument": null,
"opOutcome": null,
"opOutcomeInstrument": null,
"latitude": 55.7558,
"longitude": 37.6173
}"#;
let tx: Transaction = serde_json::from_str(json).unwrap();
assert_eq!(tx.id, TransactionId::new("tx-001".to_owned()));
assert!(!tx.deleted);
assert!((tx.outcome - 500.0).abs() < f64::EPSILON);
assert_eq!(tx.date, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
assert_eq!(tx.mcc, Some(5812));
assert!((tx.latitude.unwrap() - 55.7558).abs() < f64::EPSILON);
}
#[test]
fn deserialize_transfer_with_currency_conversion() {
let json = r#"{
"id": "tx-002",
"changed": 1700000000,
"created": 1700000000,
"user": 123,
"deleted": false,
"hold": false,
"incomeInstrument": 2,
"incomeAccount": "acc-usd",
"income": 100.0,
"outcomeInstrument": 1,
"outcomeAccount": "acc-rub",
"outcome": 9250.0,
"tag": null,
"merchant": null,
"payee": null,
"originalPayee": null,
"comment": "Currency exchange",
"date": "2024-01-15",
"mcc": null,
"reminderMarker": null,
"opIncome": 100.0,
"opIncomeInstrument": 2,
"opOutcome": 9250.0,
"opOutcomeInstrument": 1,
"latitude": null,
"longitude": null
}"#;
let tx: Transaction = serde_json::from_str(json).unwrap();
assert_eq!(tx.income_instrument, InstrumentId::new(2));
assert_eq!(tx.outcome_instrument, InstrumentId::new(1));
assert!(tx.op_income.is_some());
assert_eq!(tx.hold, Some(false));
}
#[test]
fn serialize_roundtrip() {
let tx = Transaction {
id: TransactionId::new("t-1".to_owned()),
changed: DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
created: DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
user: UserId::new(1),
deleted: false,
hold: None,
income_instrument: InstrumentId::new(1),
income_account: AccountId::new("a-1".to_owned()),
income: 0.0,
outcome_instrument: InstrumentId::new(1),
outcome_account: AccountId::new("a-1".to_owned()),
outcome: 100.0,
tag: None,
merchant: None,
payee: None,
original_payee: None,
comment: None,
date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
mcc: None,
reminder_marker: None,
op_income: None,
op_income_instrument: None,
op_outcome: None,
op_outcome_instrument: None,
latitude: None,
longitude: None,
income_bank_id: None,
outcome_bank_id: None,
qr_code: None,
source: None,
viewed: None,
};
let json = serde_json::to_string(&tx).unwrap();
let deserialized: Transaction = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, tx);
}
}