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}