Skip to main content

rhood_core/models/
document.rs

1//! Account document model types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Filterable document types accepted by the documents endpoint.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
10#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11#[serde(rename_all = "snake_case")]
12pub enum DocumentType {
13    /// Periodic account statement (monthly, quarterly).
14    AccountStatement,
15    /// Trade confirmation for a specific transaction.
16    TradeConfirm,
17    /// Consolidated 1099 tax form.
18    ///
19    /// The accepted filter value is the bare string `"1099"`, verified live:
20    /// `?type=1099` returns HTTP 200, while the previously-guessed `"tax_1099"`
21    /// returns HTTP 400.
22    #[serde(rename = "1099")]
23    #[cfg_attr(feature = "clap", value(name = "1099"))]
24    Tax1099,
25}
26
27impl fmt::Display for DocumentType {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::AccountStatement => formatter.write_str("account_statement"),
31            Self::TradeConfirm => formatter.write_str("trade_confirm"),
32            Self::Tax1099 => formatter.write_str("1099"),
33        }
34    }
35}
36
37/// An account document (statement, tax form, trade confirmation).
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Document {
40    /// Unique document identifier.
41    pub id: Option<String>,
42    /// Document type (e.g., "account_statement", "trade_confirm").
43    #[serde(rename = "type")]
44    pub document_type: Option<String>,
45    /// Date the document covers.
46    pub date: Option<String>,
47    /// Download URL.
48    pub download_url: Option<String>,
49    /// When the document was created.
50    pub created_at: Option<String>,
51    /// When the document was last updated.
52    pub updated_at: Option<String>,
53    /// API URL for the document.
54    pub url: Option<String>,
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn document_type_serializes_to_wire_form() {
63        assert_eq!(
64            serde_json::to_string(&DocumentType::AccountStatement).unwrap(),
65            "\"account_statement\""
66        );
67        assert_eq!(
68            serde_json::to_string(&DocumentType::TradeConfirm).unwrap(),
69            "\"trade_confirm\""
70        );
71        assert_eq!(
72            serde_json::to_string(&DocumentType::Tax1099).unwrap(),
73            "\"1099\""
74        );
75    }
76
77    #[test]
78    fn document_type_roundtrips_all_variants() {
79        for variant in [
80            DocumentType::AccountStatement,
81            DocumentType::TradeConfirm,
82            DocumentType::Tax1099,
83        ] {
84            let wire = serde_json::to_string(&variant).unwrap();
85            let back: DocumentType = serde_json::from_str(&wire).unwrap();
86            assert_eq!(back, variant);
87        }
88    }
89
90    #[test]
91    fn document_type_display_matches_wire() {
92        assert_eq!(
93            DocumentType::AccountStatement.to_string(),
94            "account_statement"
95        );
96        assert_eq!(DocumentType::TradeConfirm.to_string(), "trade_confirm");
97        assert_eq!(DocumentType::Tax1099.to_string(), "1099");
98    }
99}