import json
from datetime import datetime
from decimal import Decimal
from functools import cached_property
from typing import Any, Dict, List, Optional
from lxml import etree, objectify
from q2_sdk.models.recursive_encoder import RecursiveEncoder
from q2_sdk.tools.utils import to_bool, to_int
from .xml_helper import find_with_default
from .account_rights import AccountRights
from .hade import HADE
from .hydra_product import get_hydra_product_object
[docs]
class Account:
"""
Object representation of the Account information that comes in on a
Q2 Online request
Typically created by passing in the XML shape provided by HQ.
Also possible to build this by passing in kwargs.
ie. ``Account(element: lxmlElement)`` or ``Account(CifInternal="12345")``
If both are provided, kwargs will override anything inside the given element
"""
def __init__(
self,
element: Optional[objectify.Element] = None,
data_elements: Optional[List[objectify.Element]] = None,
provider_data: Optional[dict] = None,
**kwargs,
):
"""
:param element: XML node form Q2 Online request
:param data_elements: Optional list of Q2_AccountDataElement nodes.
:param provider_data: If the account originates from another system
(such as a core), this can hold additional data
that does not fit into the Q2 model.
:param kwargs: Any items provided as kwargs will override the data passed in from element
"""
assertions = (element is not None, kwargs)
assert any(assertions), (
"Either lxml Element or kwarg attributes must be specified"
)
if element is None:
element = objectify.Element("AccountListResponseRecord")
for key, value in kwargs.items():
existing = element.find(key)
if existing:
element.remove(existing)
element.append(objectify.fromstring(f"<{key}>{value}</{key}>"))
self.data_elements = data_elements
self.element = element
self.provider_data = provider_data
self._access = None
self._host_acct_id = None
self._allow_interest = None
self._hade_dict = {}
[docs]
@cached_property
def aba(self) -> Optional[str]:
return find_with_default(self.element, "Aba", data_type=str)
@property
def access(self) -> Optional[int]:
if self._access is None:
self._access = find_with_default(self.element, "Access", data_type=int)
return self._access
@access.setter
def access(self, value: int) -> None:
self._access = to_int(value)
[docs]
@cached_property
def acct_desc(self) -> Optional[str]:
return find_with_default(self.element, "AccountDesc", data_type=str)
[docs]
@cached_property
def acct_label(self) -> Optional[str]:
return find_with_default(self.element, "AccountLabel", data_type=str)
[docs]
@cached_property
def acct_number_internal(self) -> Optional[str]:
return find_with_default(self.element, "AccountNumberInternal", data_type=str)
[docs]
@cached_property
def acct_number_external(self) -> Optional[str]:
return find_with_default(self.element, "AccountNumberExternal", data_type=str)
[docs]
@cached_property
def acct_number_external_unmasked(self) -> Optional[str]:
return find_with_default(
self.element, "AccountNumberExternalUnmasked", data_type=str
)
[docs]
@cached_property
def acct_number_internal_unmasked(self) -> Optional[str]:
return find_with_default(
self.element, "AccountNumberInternalUnmasked", data_type=str
)
@property
def allow_interest(self) -> Optional[bool]:
if not self._allow_interest:
self._allow_interest = find_with_default(
self.element, "AllowInterest", data_type=bool
)
return self._allow_interest
@allow_interest.setter
def allow_interest(self, value: bool) -> None:
self._allow_interest = to_bool(value)
[docs]
@cached_property
def allow_principal(self) -> Optional[bool]:
return find_with_default(self.element, "AllowPrincipal", data_type=bool)
[docs]
@cached_property
def apr_or_apy(self) -> Optional[str]:
return find_with_default(self.element, "APRorAPY")
[docs]
@cached_property
def balance1(self) -> Optional[Decimal]:
return find_with_default(self.element, "Balance1", data_type=Decimal)
[docs]
@cached_property
def balance2(self) -> Optional[Decimal]:
return find_with_default(self.element, "Balance2", data_type=Decimal)
[docs]
@cached_property
def balance_additional_desc1(self) -> Optional[str]:
return find_with_default(self.element, "BalanceAdditionalDescription1")
[docs]
@cached_property
def balance_additional_desc2(self) -> Optional[str]:
return find_with_default(self.element, "BalanceAdditionalDescription2")
[docs]
@cached_property
def balance_desc1(self) -> Optional[str]:
return find_with_default(self.element, "BalanceDescription1")
[docs]
@cached_property
def balance_desc2(self) -> Optional[str]:
return find_with_default(self.element, "BalanceDescription2")
[docs]
@cached_property
def balance_hade_name_to_use(self) -> Optional[str]:
return find_with_default(self.element, "RunningBalanceHadeNameToUse")
[docs]
@cached_property
def balance_name1(self) -> Optional[str]:
return find_with_default(self.element, "BalanceName1")
[docs]
@cached_property
def balance_name2(self) -> Optional[str]:
return find_with_default(self.element, "BalanceName2")
[docs]
@cached_property
def balance_to_disp_tran_to_desc(self) -> Optional[str]:
return find_with_default(self.element, "BalanceToDisplayTransferToDescription")
[docs]
@cached_property
def balance_to_display(self) -> Optional[Decimal]:
return find_with_default(self.element, "BalanceToDisplay", data_type=Decimal)
[docs]
@cached_property
def balance_to_display_desc(self) -> Optional[str]:
return find_with_default(self.element, "BalanceToDisplayDescription")
[docs]
@cached_property
def balance_to_display_tran_to(self) -> Optional[Decimal]:
return find_with_default(
self.element, "BalanceToDisplayTransferTo", data_type=Decimal
)
[docs]
@cached_property
def balance_type1(self) -> Optional[str]:
return find_with_default(self.element, "BalanceType1")
[docs]
@cached_property
def balance_type2(self) -> Optional[str]:
return find_with_default(self.element, "BalanceType2")
[docs]
@cached_property
def calc_balance_for_history(self) -> Optional[bool]:
return find_with_default(
self.element, "CalculateRunningBalanceForyHistory", data_type=bool
)
[docs]
@cached_property
def calc_balance_for_memos(self) -> Optional[bool]:
return find_with_default(
self.element, "CalculateRunningBalanceForMemos", data_type=bool
)
[docs]
@cached_property
def cif(self) -> Optional[str]:
return find_with_default(self.element, "Cif", data_type=str)
[docs]
@cached_property
def cif_internal(self) -> Optional[str]:
cif = find_with_default(self.element, "CifInternal", data_type=str)
if not cif:
cif = find_with_default(self.element, "CIFInternal", data_type=str)
return cif
[docs]
@cached_property
def cif_internal_unmasked(self) -> Optional[str]:
cif = find_with_default(self.element, "CIFInternalUnmasked", data_type=str)
if not cif:
cif = find_with_default(self.element, "CifInternalUnmasked", data_type=str)
return cif
[docs]
@cached_property
def cif_external(self) -> Optional[str]:
cif = find_with_default(self.element, "CifExternal", data_type=str)
if not cif:
cif = find_with_default(self.element, "CIFExternal", data_type=str)
return cif
[docs]
@cached_property
def cif_external_unmasked(self) -> Optional[str]:
cif = find_with_default(self.element, "CIFExternalUnmasked", data_type=str)
if not cif:
cif = find_with_default(self.element, "CifExternalUnmasked", data_type=str)
return cif
[docs]
@cached_property
def data_as_of_date(self) -> Optional[datetime]:
return find_with_default(self.element, "DataAsOfDate", data_type=datetime)
[docs]
@cached_property
def default_pymt_amt_tran_from_desc(self) -> Optional[str]:
return find_with_default(
self.element, "DefaultPaymentAmountTransferFromDescription"
)
[docs]
@cached_property
def default_tran_from_pymt_amt(self) -> Optional[Decimal]:
return find_with_default(
self.element, "DefaultPaymentAmountTransferFrom", data_type=Decimal
)
[docs]
@cached_property
def display_order(self) -> Optional[int]:
return find_with_default(self.element, "DisplayOrder", data_type=int)
[docs]
@cached_property
def display_running_balance(self) -> Optional[bool]:
return find_with_default(self.element, "DisplayRunningBalance", data_type=bool)
[docs]
@cached_property
def has_pending_memos(self) -> Optional[bool]:
return find_with_default(self.element, "HasPendingMemos", data_type=bool)
[docs]
@cached_property
def history_count(self) -> Optional[int]:
return find_with_default(self.element, "HistoryCount", data_type=int)
[docs]
@cached_property
def history_count_type(self) -> Optional[str]:
return find_with_default(self.element, "HistoryCountType")
@property
def host_acct_id(self) -> Optional[int]:
if not self._host_acct_id:
self._host_acct_id = find_with_default(
self.element, "HostAccountID", data_type=int
)
return self._host_acct_id
@host_acct_id.setter
def host_acct_id(self, value: int) -> None:
self._host_acct_id = to_int(value)
[docs]
@cached_property
def hydra_product_code(self) -> Optional[str]:
return find_with_default(self.element, "HydraProductCode", data_type=str)
[docs]
@cached_property
def hydra_product_type_code(self) -> Optional[str]:
return find_with_default(self.element, "HydraProductTypeCode", data_type=str)
[docs]
@cached_property
def is_external_acct(self) -> Optional[bool]:
return find_with_default(self.element, "IsExternalAccount", data_type=bool)
[docs]
@cached_property
def is_partial_details(self) -> Optional[bool]:
return find_with_default(self.element, "IsPartialDetails", data_type=bool)
[docs]
@cached_property
def link_type(self) -> Optional[str]:
return find_with_default(self.element, "LinkType")
[docs]
@cached_property
def masked_cif_internal(self) -> Optional[str]:
return find_with_default(self.element, "MaskedCifInternal", data_type=str)
[docs]
@cached_property
def mob_dashboard_balance_desc(self) -> Optional[str]:
return find_with_default(self.element, "MobilityDashboardBalanceDescription")
[docs]
@cached_property
def mobility_dashboard_balance(self) -> Optional[Decimal]:
return find_with_default(
self.element, "MobilityDashboardBalance", data_type=Decimal
)
[docs]
@cached_property
def nickname(self) -> Optional[str]:
return find_with_default(self.element, "NickName")
[docs]
@cached_property
def overview_acct_number(self) -> str:
return find_with_default(self.element, "OverviewAccountNumber", default="")
[docs]
@cached_property
def product_id(self) -> Optional[int]:
return find_with_default(self.element, "ProductID", data_type=int)
[docs]
@cached_property
def product_name(self) -> Optional[str]:
return find_with_default(self.element, "ProductName")
[docs]
@cached_property
def product_type_id(self) -> Optional[int]:
return find_with_default(self.element, "ProductTypeID", data_type=int)
[docs]
@cached_property
def product_type_name(self) -> Optional[str]:
return find_with_default(self.element, "ProductTypeName")
[docs]
@cached_property
def product_type_voice_file(self) -> Optional[str]:
return find_with_default(self.element, "ProductTypeVoiceFile")
[docs]
@cached_property
def product_voice_file(self) -> Optional[str]:
return find_with_default(self.element, "ProductVoiceFile")
[docs]
@cached_property
def sort_value(self) -> Optional[str]:
return find_with_default(self.element, "SortValue", default="")
[docs]
@cached_property
def status_short_name(self) -> Optional[str]:
return find_with_default(self.element, "statusShortName")
[docs]
@cached_property
def user_display_order(self) -> int:
return find_with_default(
self.element, "UserDisplayOrder", data_type=int, default=0
)
[docs]
@cached_property
def user_id(self) -> Optional[int]:
return find_with_default(self.element, "UserID", data_type=int)
@property
def hade_dict(self) -> Dict[str, HADE]:
"""
Create a dict of HADE objects keyed by name for all data_elements
that match the host_acct_id
"""
if not self._hade_dict:
data_elements = self.data_elements
if data_elements is None:
data_elements = []
self._hade_dict = {
x.HADEName: HADE(x)
for x in data_elements
if x.HostAccountID.text == str(self.host_acct_id)
}
return self._hade_dict
[docs]
@cached_property
def can_deposit(self) -> bool:
"""Return bool indicating whether this account's access level allows deposit"""
return (self.access & AccountRights.Deposit) == AccountRights.Deposit
[docs]
@cached_property
def can_view(self) -> bool:
"""Return bool indicating whether this account's access level allows view"""
return (self.access & AccountRights.View) == AccountRights.View
[docs]
@cached_property
def can_withdraw(self) -> bool:
"""Return bool indicating whether this account's access level allows withdrawal"""
return (self.access & AccountRights.Withdraw) == AccountRights.Withdraw
[docs]
@cached_property
def hydra_product_name(self) -> str:
"""Returns a generic product name for the account"""
hydra_product = get_hydra_product_object(
self.hydra_product_type_code, self.hydra_product_code
)
return hydra_product.name
[docs]
@staticmethod
def from_json(data) -> "Account":
element = objectify.fromstring(data["element"])
data_elements = data.get("data_elements")
if data_elements is not None:
data_elements = [objectify.fromstring(x) for x in data_elements]
provider_data = data.get("provider_data")
if isinstance(provider_data, str):
provider_data = json.loads(provider_data)
account = Account(
element, provider_data=provider_data, data_elements=data_elements
)
return account
[docs]
def to_json(self) -> Dict[str, Any]:
return json.loads(RecursiveEncoder().encode(self))
def __json__(self):
return {
"element": etree.tostring(self.element).decode(),
"provider_data": json.dumps(self.provider_data),
"data_elements": [etree.tostring(x).decode() for x in self.data_elements],
}
def __repr__(self) -> Optional[str]:
return "<Object Account: {}>".format(self.host_acct_id)