akahu_client/models/
account.rs

1//! Rust structs representing the Account Model Akahu uses, the documentation
2//! for the Akahu model this is derived from is
3//! [here](https://developers.akahu.nz/docs/the-account-model).
4
5use serde::{Deserialize, Serialize};
6
7use crate::{AccountId, AuthorizationId, BankAccountNumber};
8
9/// An Akahu account is something that has a balance. Some connections (like
10/// banks) have lots of accounts, while others (like KiwiSaver providers) may
11/// only have one. Different types of accounts have different attributes and
12/// abilities, which can get a bit confusing! The rest of this page should help
13/// you figure everything out, from an account's provider to whether it can make
14/// payments.
15///
16/// Keep in mind that we limit what information is available depending on your
17/// app permissions. This is done in order to protect user privacy, however it
18/// also means that some of the data here may not be visible to you.
19#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
20pub struct Account {
21    /// The `id` key is a unique identifier for the account in the Akahu system.
22    ///
23    /// It is always be prefixed by acc_ so that you can tell that it belongs to
24    /// an account.
25    ///
26    /// [<https://developers.akahu.nz/docs/the-account-model#_id>]
27    #[serde(rename = "_id")]
28    pub id: AccountId,
29
30    /// The identifier of this account's predecessor.
31    ///
32    /// This attribute is only present if the account has been migrated to an
33    /// official open banking connection from a classic Akahu connection.
34    ///
35    /// Read more about official open banking, and migrating to it
36    /// [here](https://developers.akahu.nz/docs/official-open-banking).
37    ///
38    /// [<https://developers.akahu.nz/docs/the-account-model#_migrated>]
39    #[serde(default, skip_serializing_if = "Option::is_none", rename = "_migrated")]
40    pub migrated: Option<String>,
41
42    /// Financial accounts are connected to Akahu via an authorisation with the
43    /// user's financial institution. Multiple accounts can be connected during
44    /// a single authorisation, causing them to have the same authorisation
45    /// identifier. This identifier can also be used to link a specific account
46    /// to identity data for the
47    /// [party](https://developers.akahu.nz/reference/get_parties) who completed
48    /// the authorisation.
49    ///
50    /// This identifier can also be used to revoke access to all the accounts
51    /// connected to that authorisation.
52    ///
53    /// For example, if you have 3 ANZ accounts, they will all have the same
54    /// `authorisation`. Your ANZ accounts and your friend's ANZ accounts have
55    /// different logins, so they will have a different `authorisation key`. The
56    /// `authorisation` key is in no way derived or related to your login
57    /// credentials - it's just a random ID.
58    ///
59    /// [<https://developers.akahu.nz/docs/the-account-model#_authorisation>]
60    #[serde(rename = "_authorisation")]
61    pub authorisation: AuthorizationId,
62
63    /// Deprecated: Please use `authorisation` instead.
64    ///
65    /// [<https://developers.akahu.nz/docs/the-account-model#_credentials-deprecated>]
66    #[deprecated(note = "Please use `authorisation` instead.")]
67    #[serde(
68        rename = "_credentials",
69        default,
70        skip_serializing_if = "Option::is_none"
71    )]
72    pub credentials: Option<AuthorizationId>,
73
74    /// This is the name of the account. If the connection allows customisation,
75    /// the name will be the custom name (or nickname), e.g. "Spending Account".
76    /// Otherwise Akahu falls back to the product name, e.g. "Super Saver".
77    ///
78    /// [<https://developers.akahu.nz/docs/the-account-model#name>]
79    pub name: String,
80
81    /// This attribute indicates the status of Akahu's connection to this account.
82    ///
83    /// It is possible for Akahu to lose the ability to authenticate with a
84    /// financial institution if the user revokes Akahu's access directly via
85    /// their institution, or changes their login credentials, which in some
86    /// cases can cause our long-lived access to be revoked.
87    ///
88    /// [<https://developers.akahu.nz/docs/the-account-model#status>]
89    pub status: Active,
90
91    /// If the account has a well defined account number (eg. a bank account
92    /// number, or credit card number) this will be defined here with a standard
93    /// format across connections. This field will be the value undefined for
94    /// accounts with KiwiSaver providers and investment platform accounts.
95    ///
96    /// For NZ banks, we use the common format 00-0000-0000000-00. For credit
97    /// cards, we return a redacted card number 1234-****-****-1234 or
98    /// ****-****-****-1234
99    ///
100    /// [<https://developers.akahu.nz/docs/the-account-model#formatted_account>]
101    // TODO: could hyave a strongly defined type here.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub formatted_acount: Option<String>,
104
105    /// Akahu can refresh different parts of an account's data at different rates.
106    /// The timestamps in the refreshed object tell you when that account data was
107    /// last updated.
108    ///
109    /// When looking at a timestamp in here, you can think "Akahu's view of the
110    /// account (balance/metadata/transactions) is up to date as of $TIME".
111    ///
112    /// [<https://developers.akahu.nz/docs/the-account-model#refreshed>]
113    pub refreshed: RefreshDetails,
114
115    /// The account balance.
116    ///
117    /// [<https://developers.akahu.nz/docs/the-account-model#balance>]
118    pub balance: BalanceDetails,
119
120    /// What sort of account this is. Akahu provides specific bank account
121    /// types, and falls back to more general types for other types of
122    /// connection.
123    ///
124    /// [<https://developers.akahu.nz/docs/the-account-model#type>]
125    #[serde(rename = "type")]
126    pub kind: BankAccountKind,
127
128    /// The list of attributes indicates what abilities an account has.
129    ///
130    /// See [Attribute] for more information.
131    ///
132    /// [<https://developers.akahu.nz/docs/the-account-model#attributes>]
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub attributes: Vec<Attribute>,
135}
136
137/// This attribute indicates the status of Akahu's connection to this account.
138///
139/// It is possible for Akahu to lose the ability to authenticate with a
140/// financial institution if the user revokes Akahu's access directly via
141/// their institution, or changes their login credentials, which in some
142/// cases can cause our long-lived access to be revoked.
143///
144/// [<https://developers.akahu.nz/docs/the-account-model#status>]
145#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
146#[serde(rename_all = "UPPERCASE")]
147pub enum Active {
148    /// Akahu can authenticate with the institution to retrieve data
149    /// and/or initiate payments for this account.
150    Active,
151    /// Akahu no longer has access to this account. Your
152    /// application will still be able to access Akahu's cached copy of data for
153    /// this account, but this will no longer be updated by
154    /// [refreshes](https://developers.akahu.nz/docs/data-refreshes). Write
155    /// actions such as payments or transfers will no longer be available. Once
156    /// an account is assigned the INACTIVE status, it will stay this way until
157    /// the user re-establishes the connection. When your application observes
158    /// an account with a status of INACTIVE, the user should be directed back
159    /// to the Akahu OAuth flow or to [<https://my.akahu.nz/connections>] where
160    /// they will be prompted to re-establish the connection.
161    Inactive,
162}
163
164impl Active {
165    /// Get the active status as a string slice.
166    pub const fn as_str(&self) -> &'static str {
167        match self {
168            Self::Active => "ACTIVE",
169            Self::Inactive => "INACTIVE",
170        }
171    }
172
173    /// Get the active status as bytes.
174    pub const fn as_bytes(&self) -> &'static [u8] {
175        self.as_str().as_bytes()
176    }
177}
178
179impl std::str::FromStr for Active {
180    type Err = ();
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        match s {
183            "ACTIVE" => Ok(Self::Active),
184            "INACTIVE" => Ok(Self::Inactive),
185            _ => Err(()),
186        }
187    }
188}
189
190impl std::convert::TryFrom<String> for Active {
191    type Error = ();
192    fn try_from(value: String) -> Result<Self, Self::Error> {
193        value.parse()
194    }
195}
196
197impl std::convert::TryFrom<&str> for Active {
198    type Error = ();
199    fn try_from(value: &str) -> Result<Self, Self::Error> {
200        value.parse()
201    }
202}
203
204impl std::fmt::Display for Active {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        write!(f, "{}", self.as_str())
207    }
208}
209
210/// This is a less defined part of our API that lets us expose data that may be
211/// specific to certain account types or financial institutions. An investment
212/// provider, for example, may expose a breakdown of investment results.
213///
214/// Akahu standardises this metadata as much as possible. However depending on
215/// the specific integration and account, some data fields may be unavailable or
216/// poorly specified. Treat all fields in the meta object as optional.
217///
218/// [<https://developers.akahu.nz/docs/the-account-model#meta>]
219#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
220pub struct AccountMetadata {
221    /// The account holder name as exposed by the provider. In the case of bank
222    /// accounts this is the name on the bank statement.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub holder: Option<String>,
225
226    /// Indicates if the account has other holders that are not listed in the
227    /// holder field. This only applies to official open banking connections
228    /// where the institution indicates a joint account, but only provides the
229    /// authorising party's name.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub has_unlisted_holders: Option<bool>,
232
233    /// If the account can be paid but is not a bank account (for example a
234    /// KiwiSaver account), this field will have payment details.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub payment_details: Option<PaymentDetails>,
237
238    /// Includes detailed information related to a loan account (if available
239    /// from the loan provider).
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub loan_details: Option<LoanDetails>,
242
243    /// An investment breakdown. Details are passed straight through from
244    /// integrations, making them very inconsistent.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub breakdown: Option<serde_json::Value>,
247
248    /// An investment portfolio. Details are passed through from integrations,
249    /// so some are missing various fields. A maximum of 200 funds/instruments
250    /// are supported per investment account.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub portfolio: Option<serde_json::Value>,
253}
254
255/// Details for making a payment to an account that is not a bank account.
256#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
257pub struct PaymentDetails {
258    /// The recipient's name.
259    pub account_holder: String,
260    /// The recipient's NZ bank account number.
261    pub account_number: BankAccountNumber,
262    /// Details required to be in the payment particulars.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub particulars: Option<String>,
265    /// Details required to be in the payment code.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub code: Option<String>,
268    /// Details required to be in the payment reference.
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub reference: Option<String>,
271    /// If there is a minimum amount in order to have the payment accepted, in
272    /// dollars.
273    #[serde(
274        default,
275        skip_serializing_if = "Option::is_none",
276        with = "rust_decimal::serde::arbitrary_precision_option"
277    )]
278    pub minimum_amount: Option<rust_decimal::Decimal>,
279}
280
281/// Detailed information related to a loan account.
282#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
283pub struct LoanDetails {
284    /// The purpose of the loan (E.g. HOME), if we can't determine the purpose,
285    /// this will be UNKNOWN.
286    pub purpose: String,
287    /// The type of loan (E.g. TABLE), if we can't determine the type, this will
288    /// be UNKNOWN.
289    // TODO: Could be an enum but we do not know all possible classifications.
290    #[serde(rename = "type")]
291    pub loan_type: String,
292    /// Interest rate information for the loan.
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub interest: Option<InterestDetails>,
295    /// Is the loan currently in an interest only period?
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub is_interest_only: Option<bool>,
298    /// When the interest only period expires, if available.
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub interest_only_expires_at: Option<chrono::DateTime<chrono::Utc>>,
301    /// The duration/term of the loan for it to be paid to completion from the
302    /// start date of the loan.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub term: Option<String>,
305    /// When the loan matures, if available.
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub matures_at: Option<chrono::DateTime<chrono::Utc>>,
308    /// The loan initial principal amount, this was the original amount
309    /// borrowed.
310    #[serde(
311        default,
312        skip_serializing_if = "Option::is_none",
313        with = "rust_decimal::serde::arbitrary_precision_option"
314    )]
315    pub initial_principal: Option<rust_decimal::Decimal>,
316    /// Loan repayment information if available.
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub repayment: Option<RepaymentDetails>,
319}
320
321/// Interest rate information for a loan.
322#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
323pub struct InterestDetails {
324    /// The rate of interest.
325    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
326    pub rate: rust_decimal::Decimal,
327    /// The type of interest rate (E.g. FIXED).
328    // TODO: Could be an enum but we do not know all possible classifications.
329    #[serde(rename = "type")]
330    pub interest_type: String,
331    /// When this interest rate expires, if available.
332    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
333}
334
335/// Loan repayment information.
336#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
337pub struct RepaymentDetails {
338    /// The frequency of the loan repayment (E.g. MONTHLY).
339    pub frequency: String,
340    /// The next repayment date, if available.
341    pub next_date: Option<chrono::DateTime<chrono::Utc>>,
342    /// The next instalment amount.
343    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
344    pub next_amount: rust_decimal::Decimal,
345}
346
347/// Akahu can refresh different parts of an account's data at different rates.
348/// The timestamps in the refreshed object tell you when that account data was
349/// last updated.
350///
351/// When looking at a timestamp in here, you can think "Akahu's view of the
352/// account (balance/metadata/transactions) is up to date as of $TIME".
353///
354/// [<https://developers.akahu.nz/docs/the-account-model#refreshed>]
355#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
356pub struct RefreshDetails {
357    /// When the balance was last updated.
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub balance: Option<chrono::DateTime<chrono::Utc>>,
360
361    /// When other account metadata was last updated (any account property apart
362    /// from balance).
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub meta: Option<chrono::DateTime<chrono::Utc>>,
365
366    /// When we last checked for and processed any new transactions.
367    ///
368    /// This flag may be missing when an account has first connected, as it
369    /// takes a few seconds for new transactions to be processed.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub transactions: Option<chrono::DateTime<chrono::Utc>>,
372
373    /// When we last fetched identity data about the
374    /// [party](https://developers.akahu.nz/docs/enduring-identity-verification#party-data)
375    /// who has authenticated with the financial institution when connecting
376    /// this account.
377    ///
378    /// This data is updated by Akahu on a fixed 30 day interval, regardless of
379    /// your app's [data
380    /// refresh](https://developers.akahu.nz/docs/data-refreshes) configuration.
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub party: Option<chrono::DateTime<chrono::Utc>>,
383}
384
385/// The account balance.
386///
387/// [<https://developers.akahu.nz/docs/the-account-model#balance>]
388#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
389pub struct BalanceDetails {
390    /// The current account balance.
391    ///
392    /// A negative balance indicates the amount owed to the account issuer. For
393    /// example a checking account in overdraft will have a negative balance,
394    /// same as the amount owed on a credit card or the principal remaining on a
395    /// loan.
396    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
397    pub current: rust_decimal::Decimal,
398
399    /// The balance that is currently available to the account holder.
400    #[serde(
401        default,
402        skip_serializing_if = "Option::is_none",
403        with = "rust_decimal::serde::arbitrary_precision_option"
404    )]
405    pub available: Option<rust_decimal::Decimal>,
406
407    /// The credit limit for this account.
408    ///
409    /// For example a credit card limit or an overdraft limit. This value is
410    /// only present when provided directly by the connected financial
411    /// institution.
412    #[serde(
413        default,
414        skip_serializing_if = "Option::is_none",
415        with = "rust_decimal::serde::arbitrary_precision_option"
416    )]
417    pub limit: Option<rust_decimal::Decimal>,
418
419    /// A boolean indicating whether this account is in overdraft.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub overdrawn: Option<bool>,
422
423    /// The [3 letter ISO 4217 currency code](https://www.xe.com/iso4217.php)
424    /// that this balance is in (e.g. NZD).
425    pub currency: iso_currency::Currency,
426}
427
428/// What sort of account this is. Akahu provides specific bank account types,
429/// and falls back to more general types for other types of connection.
430///
431/// [<https://developers.akahu.nz/docs/the-account-model#type>]
432#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
433#[serde(rename_all = "UPPERCASE")]
434pub enum BankAccountKind {
435    /// An everyday spending account.
436    Checking,
437    /// A savings account.
438    ///
439    /// NOTE: A savings account is not necessarily a regular bank account. It might
440    /// not have transactions associated, or be able to receive payments. Check
441    /// the attributes field to see what this account can do.
442    Savings,
443    /// A credit card.
444    #[serde(rename = "CREDITCARD")]
445    CreditCard,
446    /// A loan account.
447    Loan,
448    /// A KiwiSaver investment product.
449    Kiwisaver,
450    /// A general investment product.
451    Investment,
452    /// A term deposit.
453    #[serde(rename = "TERMDEPOSIT")]
454    TermDeposit,
455    /// An account holding a foreign currency.
456    Foreign,
457    /// An account with tax authorities.
458    Tax,
459    /// An account for rewards points, e.g. Fly Buys or True Rewards.
460    Rewards,
461    /// Available cash for investment or withdrawal from an investment provider.
462    Wallet,
463}
464
465impl BankAccountKind {
466    /// Get the bank account kind as a string slice.
467    pub const fn as_str(&self) -> &'static str {
468        match self {
469            Self::Checking => "CHECKING",
470            Self::Savings => "SAVINGS",
471            Self::CreditCard => "CREDITCARD",
472            Self::Loan => "LOAN",
473            Self::Kiwisaver => "KIWISAVER",
474            Self::Investment => "INVESTMENT",
475            Self::TermDeposit => "TERMDEPOSIT",
476            Self::Foreign => "FOREIGN",
477            Self::Tax => "TAX",
478            Self::Rewards => "REWARDS",
479            Self::Wallet => "WALLET",
480        }
481    }
482
483    /// Get the bank account kind as bytes.
484    pub const fn as_bytes(&self) -> &'static [u8] {
485        self.as_str().as_bytes()
486    }
487}
488
489impl std::str::FromStr for BankAccountKind {
490    type Err = ();
491    fn from_str(s: &str) -> Result<Self, Self::Err> {
492        match s {
493            "CHECKING" => Ok(Self::Checking),
494            "SAVINGS" => Ok(Self::Savings),
495            "CREDITCARD" => Ok(Self::CreditCard),
496            "LOAN" => Ok(Self::Loan),
497            "KIWISAVER" => Ok(Self::Kiwisaver),
498            "INVESTMENT" => Ok(Self::Investment),
499            "TERMDEPOSIT" => Ok(Self::TermDeposit),
500            "FOREIGN" => Ok(Self::Foreign),
501            "TAX" => Ok(Self::Tax),
502            "REWARDS" => Ok(Self::Rewards),
503            "WALLET" => Ok(Self::Wallet),
504            _ => Err(()),
505        }
506    }
507}
508
509impl std::convert::TryFrom<String> for BankAccountKind {
510    type Error = ();
511    fn try_from(value: String) -> Result<Self, Self::Error> {
512        value.parse()
513    }
514}
515
516impl std::convert::TryFrom<&str> for BankAccountKind {
517    type Error = ();
518    fn try_from(value: &str) -> Result<Self, Self::Error> {
519        value.parse()
520    }
521}
522
523impl std::fmt::Display for BankAccountKind {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        write!(f, "{}", self.as_str())
526    }
527}
528
529/// The list of attributes indicates what abilities an account has.
530///
531/// [<https://developers.akahu.nz/docs/the-account-model#attributes>]
532#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
533#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
534pub enum Attribute {
535    /// Akahu can fetch available transactions from this account.
536    Transactions,
537    /// This account can receive transfers from accounts belonging to the same
538    /// set of credentials.
539    TransferTo,
540    /// This account can initiate transfers to accounts belonging to the same
541    /// set of credentials.
542    TransferFrom,
543    /// This account can receive payments from another bank account.
544    PaymentTo,
545    /// This account can initiate payments to another bank account.
546    PaymentFrom,
547}
548
549impl Attribute {
550    /// Get the attribute as a string slice.
551    pub const fn as_str(&self) -> &'static str {
552        match self {
553            Self::Transactions => "TRANSACTIONS",
554            Self::TransferTo => "TRANSFER_TO",
555            Self::TransferFrom => "TRANSFER_FROM",
556            Self::PaymentTo => "PAYMENT_TO",
557            Self::PaymentFrom => "PAYMENT_FROM",
558        }
559    }
560
561    /// Get the attribute as bytes.
562    pub const fn as_bytes(&self) -> &'static [u8] {
563        self.as_str().as_bytes()
564    }
565}
566
567impl std::str::FromStr for Attribute {
568    type Err = ();
569    fn from_str(s: &str) -> Result<Self, Self::Err> {
570        match s {
571            "TRANSACTIONS" => Ok(Self::Transactions),
572            "TRANSFER_TO" => Ok(Self::TransferTo),
573            "TRANSFER_FROM" => Ok(Self::TransferFrom),
574            "PAYMENT_TO" => Ok(Self::PaymentTo),
575            "PAYMENT_FROM" => Ok(Self::PaymentFrom),
576            _ => Err(()),
577        }
578    }
579}
580
581impl std::convert::TryFrom<String> for Attribute {
582    type Error = ();
583    fn try_from(value: String) -> Result<Self, Self::Error> {
584        value.parse()
585    }
586}
587
588impl std::convert::TryFrom<&str> for Attribute {
589    type Error = ();
590    fn try_from(value: &str) -> Result<Self, Self::Error> {
591        value.parse()
592    }
593}
594
595impl std::fmt::Display for Attribute {
596    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597        write!(f, "{}", self.as_str())
598    }
599}