sure_client_rs/models/
account.rs

1use crate::{serde::deserialize_flexible_decimal, types::AccountId};
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6use url::Url;
7
8/// The kind of an account.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "PascalCase")]
11pub enum AccountKind {
12    /// A depository account, such as a checking or savings account.
13    #[serde(alias = "depository")]
14    Depository,
15    /// A credit card account.
16    #[serde(alias = "credit_card")]
17    CreditCard,
18    /// An investment account, such as a brokerage or retirement account.
19    #[serde(alias = "investment")]
20    Investment,
21    /// A property asset, such as real estate or a vehicle.
22    #[serde(alias = "property")]
23    Property,
24    /// A loan or debt account, such as a mortgage or student loan.
25    #[serde(alias = "loan")]
26    Loan,
27    /// Any other type of asset not covered by other kinds.
28    #[serde(alias = "other_asset")]
29    OtherAsset,
30    /// Any other type of liability not covered by other kinds.
31    #[serde(alias = "other_liability")]
32    OtherLiability,
33}
34
35impl std::fmt::Display for AccountKind {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        let s = match self {
38            Self::Depository => "Depository",
39            Self::CreditCard => "CreditCard",
40            Self::Investment => "Investment",
41            Self::Property => "Property",
42            Self::Loan => "Loan",
43            Self::OtherAsset => "OtherAsset",
44            Self::OtherLiability => "OtherLiability",
45        };
46        write!(f, "{}", s)
47    }
48}
49
50/// Error returned when parsing an `AccountKind` from a string fails.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ParseAccountKindError(String);
53
54impl std::fmt::Display for ParseAccountKindError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "Invalid account kind: {}", self.0)
57    }
58}
59
60impl std::error::Error for ParseAccountKindError {}
61
62impl std::str::FromStr for AccountKind {
63    type Err = ParseAccountKindError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        match s {
67            "Depository" => Ok(Self::Depository),
68            "CreditCard" => Ok(Self::CreditCard),
69            "Investment" => Ok(Self::Investment),
70            "Property" => Ok(Self::Property),
71            "Loan" => Ok(Self::Loan),
72            "OtherAsset" => Ok(Self::OtherAsset),
73            "OtherLiability" => Ok(Self::OtherLiability),
74            _ => Err(ParseAccountKindError(s.to_string())),
75        }
76    }
77}
78
79impl TryFrom<&str> for AccountKind {
80    type Error = ParseAccountKindError;
81
82    fn try_from(value: &str) -> Result<Self, Self::Error> {
83        value.parse()
84    }
85}
86
87/// Account information
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
90pub struct Account {
91    /// Unique identifier
92    pub id: AccountId,
93    /// Account name
94    pub name: String,
95    /// Unformatted balance
96    #[serde(deserialize_with = "deserialize_flexible_decimal")]
97    pub balance: Decimal,
98    /// Currency code (e.g. "USD")
99    pub currency: iso_currency::Currency,
100    /// Account classification (e.g. "asset", "liability")
101    pub classification: String,
102    /// Account kind
103    #[serde(rename = "account_type")]
104    pub kind: AccountKind,
105}
106
107/// Detailed account information
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
110pub struct AccountDetail {
111    /// Unique identifier
112    pub id: AccountId,
113    /// Account name
114    pub name: String,
115    /// Unformatted balance
116    #[serde(deserialize_with = "deserialize_flexible_decimal")]
117    pub balance: Decimal,
118    /// Currency code (e.g. "USD")
119    pub currency: iso_currency::Currency,
120    /// Account classification (e.g. "asset", "liability")
121    pub classification: String,
122    /// Account kind
123    #[serde(rename = "account_type")]
124    pub kind: AccountKind,
125    /// Account subtype (e.g. "checking", "savings")
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub subtype: Option<String>,
128    /// Name of the financial institution
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub institution_name: Option<String>,
131    /// Domain of the financial institution
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub institution_domain: Option<String>,
134    /// Additional notes about the account
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub notes: Option<String>,
137    /// Whether the account is active
138    pub is_active: bool,
139    /// Creation timestamp
140    pub created_at: DateTime<Utc>,
141    /// Last update timestamp
142    pub updated_at: DateTime<Utc>,
143}
144
145/// Collection of accounts
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
148pub struct AccountCollection {
149    /// List of accounts
150    pub accounts: Vec<Account>,
151}
152
153/// Request to create a new account
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
156pub(crate) struct CreateAccountRequest {
157    /// Account data
158    pub account: CreateAccountData,
159}
160
161/// Data for creating a new account
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
164pub(crate) struct CreateAccountData {
165    /// Account name
166    pub name: String,
167    /// Account kind
168    #[serde(rename = "accountable_type")]
169    pub kind: AccountKind,
170    /// Initial account balance
171    pub balance: Decimal,
172    /// Currency code (defaults to family currency if not provided)
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub currency: Option<iso_currency::Currency>,
175    /// Name of the financial institution
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub institution_name: Option<String>,
178    /// Domain of the financial institution
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub institution_domain: Option<Url>,
181    /// Additional notes
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub notes: Option<String>,
184    /// Type-specific attributes (required, must match the account kind)
185    pub accountable_attributes: AccountableAttributes,
186}
187
188/// Request to update an existing account
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
191pub(crate) struct UpdateAccountRequest {
192    /// Account data
193    pub account: UpdateAccountData,
194}
195
196/// Data for updating an account
197#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
198#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
199pub(crate) struct UpdateAccountData {
200    /// Account name
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub name: Option<String>,
203    /// Updates the current balance of the account
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub balance: Option<Decimal>,
206    /// Name of the financial institution
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub institution_name: Option<String>,
209    /// Domain of the financial institution
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub institution_domain: Option<Url>,
212    /// Additional notes
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub notes: Option<String>,
215    /// Type-specific attributes (optional, must match the account kind if provided)
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub accountable_attributes: Option<AccountableAttributes>,
218}
219
220// ==================== Type-specific Account Attributes ====================
221
222/// Subtype for depository accounts
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum DepositorySubtype {
226    /// Checking account
227    Checking,
228    /// Savings account
229    Savings,
230    /// Health Savings Account
231    Hsa,
232    /// Certificate of Deposit
233    Cd,
234    /// Money market account
235    MoneyMarket,
236}
237
238/// Attributes for depository (cash) accounts
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
241pub struct DepositoryAttributes {
242    /// Account subtype (e.g., checking, savings)
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub subtype: Option<DepositorySubtype>,
245    /// Attributes that should not be overwritten by syncs
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub locked_attributes: Option<JsonValue>,
248}
249
250/// Subtype for investment accounts
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "snake_case")]
253pub enum InvestmentSubtype {
254    /// Standard brokerage account
255    Brokerage,
256    /// Pension account
257    Pension,
258    /// General retirement account
259    Retirement,
260    /// 401(k) retirement plan
261    #[serde(rename = "401k")]
262    FourZeroOneK,
263    /// Roth 401(k) retirement plan
264    #[serde(rename = "roth_401k")]
265    RothFourZeroOneK,
266    /// 403(b) retirement plan
267    #[serde(rename = "403b")]
268    FourZeroThreeB,
269    /// Thrift Savings Plan
270    Tsp,
271    /// 529 education savings plan
272    #[serde(rename = "529_plan")]
273    FiveTwoNinePlan,
274    /// Health Savings Account
275    Hsa,
276    /// Mutual fund account
277    MutualFund,
278    /// Traditional IRA
279    Ira,
280    /// Roth IRA
281    RothIra,
282    /// Angel investment account
283    Angel,
284}
285
286/// Attributes for investment accounts
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
289pub struct InvestmentAttributes {
290    /// Account subtype
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub subtype: Option<InvestmentSubtype>,
293    /// Attributes that should not be overwritten by syncs
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub locked_attributes: Option<JsonValue>,
296}
297
298/// Attributes for cryptocurrency accounts
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
301pub struct CryptoAttributes {
302    /// Account subtype (no predefined values)
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub subtype: Option<String>,
305    /// Attributes that should not be overwritten by syncs
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub locked_attributes: Option<JsonValue>,
308}
309
310/// Subtype for property assets
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "snake_case")]
313pub enum PropertySubtype {
314    /// Single family home
315    SingleFamilyHome,
316    /// Multi-family home
317    MultiFamilyHome,
318    /// Condominium
319    Condominium,
320    /// Townhouse
321    Townhouse,
322    /// Investment property
323    InvestmentProperty,
324    /// Second home
325    SecondHome,
326}
327
328/// Address information for property
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
331pub struct Address {
332    /// Address line 1
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub line1: Option<String>,
335    /// Address line 2
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub line2: Option<String>,
338    /// City or locality
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub locality: Option<String>,
341    /// State or region
342    #[serde(default, skip_serializing_if = "Option::is_none")]
343    pub region: Option<String>,
344    /// Postal code
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub postal_code: Option<String>,
347    /// Country
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub country: Option<String>,
350}
351
352/// Attributes for property assets
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
355pub struct PropertyAttributes {
356    /// Property subtype
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub subtype: Option<PropertySubtype>,
359    /// Year the property was built
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub year_built: Option<i32>,
362    /// Property area value
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub area_value: Option<i32>,
365    /// Property area unit (default: "sqft")
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub area_unit: Option<String>,
368    /// Attributes that should not be overwritten by syncs
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub locked_attributes: Option<JsonValue>,
371    /// Property address
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub address_attributes: Option<Address>,
374}
375
376/// Attributes for vehicle assets
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
379pub struct VehicleAttributes {
380    /// Vehicle year
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub year: Option<i32>,
383    /// Vehicle make (e.g., Toyota)
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub make: Option<String>,
386    /// Vehicle model (e.g., Camry)
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub model: Option<String>,
389    /// Vehicle mileage value
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub mileage_value: Option<i32>,
392    /// Vehicle mileage unit (default: "mi")
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub mileage_unit: Option<String>,
395    /// Vehicle subtype (no predefined values)
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub subtype: Option<String>,
398    /// Attributes that should not be overwritten by syncs
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub locked_attributes: Option<JsonValue>,
401}
402
403/// Attributes for other asset types
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
406pub struct OtherAssetAttributes {
407    /// Account subtype (no predefined values)
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    pub subtype: Option<String>,
410    /// Attributes that should not be overwritten by syncs
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    pub locked_attributes: Option<JsonValue>,
413}
414
415/// Attributes for credit card liabilities
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
418pub struct CreditCardAttributes {
419    /// Credit card subtype (only "credit_card" is predefined)
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub subtype: Option<String>,
422    /// Available credit amount
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub available_credit: Option<Decimal>,
425    /// Minimum payment amount
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub minimum_payment: Option<Decimal>,
428    /// Annual Percentage Rate
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub apr: Option<Decimal>,
431    /// Card expiration date
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub expiration_date: Option<DateTime<Utc>>,
434    /// Annual fee amount
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub annual_fee: Option<Decimal>,
437    /// Attributes that should not be overwritten by syncs
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub locked_attributes: Option<JsonValue>,
440}
441
442/// Subtype for loan liabilities
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444#[serde(rename_all = "snake_case")]
445pub enum LoanSubtype {
446    /// Mortgage loan
447    Mortgage,
448    /// Student loan
449    Student,
450    /// Auto loan
451    Auto,
452    /// Other loan type
453    Other,
454}
455
456/// Rate type for loans
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458#[serde(rename_all = "snake_case")]
459pub enum LoanRateType {
460    /// Fixed interest rate
461    Fixed,
462    /// Variable interest rate
463    Variable,
464}
465
466/// Attributes for loan liabilities
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
469pub struct LoanAttributes {
470    /// Loan subtype
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub subtype: Option<LoanSubtype>,
473    /// Interest rate type (fixed or variable)
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub rate_type: Option<LoanRateType>,
476    /// Interest rate percentage
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub interest_rate: Option<Decimal>,
479    /// Loan term in months
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub term_months: Option<i32>,
482    /// Initial loan balance (deprecated - use first valuation instead)
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub initial_balance: Option<Decimal>,
485    /// Attributes that should not be overwritten by syncs
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub locked_attributes: Option<JsonValue>,
488}
489
490/// Attributes for other liability types
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
493pub struct OtherLiabilityAttributes {
494    /// Account subtype (no predefined values)
495    #[serde(default, skip_serializing_if = "Option::is_none")]
496    pub subtype: Option<String>,
497    /// Attributes that should not be overwritten by syncs
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub locked_attributes: Option<JsonValue>,
500}
501
502/// Type-specific attributes for different account kinds.
503///
504/// The enum variant must match the `AccountKind` used when creating the account.
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(untagged)]
507pub enum AccountableAttributes {
508    /// Depository account attributes
509    Depository(DepositoryAttributes),
510    /// Investment account attributes
511    Investment(InvestmentAttributes),
512    /// Cryptocurrency account attributes
513    Crypto(CryptoAttributes),
514    /// Property asset attributes
515    Property(PropertyAttributes),
516    /// Vehicle asset attributes (note: API uses "Property" kind with vehicle data)
517    Vehicle(VehicleAttributes),
518    /// Other asset attributes
519    OtherAsset(OtherAssetAttributes),
520    /// Credit card liability attributes
521    CreditCard(CreditCardAttributes),
522    /// Loan liability attributes
523    Loan(LoanAttributes),
524    /// Other liability attributes
525    OtherLiability(OtherLiabilityAttributes),
526}
527
528impl AccountableAttributes {
529    /// Returns the `AccountKind` that corresponds to these attributes.
530    pub const fn kind(&self) -> AccountKind {
531        match self {
532            Self::Depository(_) => AccountKind::Depository,
533            Self::Investment(_) => AccountKind::Investment,
534            Self::Crypto(_) => AccountKind::Property, // Crypto uses Property kind
535            Self::Property(_) => AccountKind::Property,
536            Self::Vehicle(_) => AccountKind::Property, // Vehicle uses Property kind
537            Self::OtherAsset(_) => AccountKind::OtherAsset,
538            Self::CreditCard(_) => AccountKind::CreditCard,
539            Self::Loan(_) => AccountKind::Loan,
540            Self::OtherLiability(_) => AccountKind::OtherLiability,
541        }
542    }
543}