from __future__ import annotations
from dataclasses import dataclass
import re
from dateutil import parser
from q2_sdk.models.recursive_encoder import JsonSerializable
EMAIL_REGEX = re.compile(r"[^@]+@[^\.]+\..+")
[docs]
class PhoneType:
"""Enum for Q2 Phone Types"""
PERSONAL = "Home"
BUSINESS = "Business"
CELL = "Mobile"
OTHER = "Other"
[docs]
class Phone(JsonSerializable):
"""Standard Phone class so developers don't all roll their own"""
def __init__(
self,
area_code: str,
phone_number: str,
phone_type: PhoneType,
extension="",
country: str | None = None,
additional_details: dict | None = None,
is_sac_target: bool = True,
is_sac_sms_target: bool = True,
full_phone_number="",
):
if extension is None:
extension = ""
self.area_code = area_code
self.phone_number = phone_number
self.type = phone_type
self.extension = extension.strip()
self.country = "USA" if not country else country
if not additional_details:
additional_details = {}
self.additional_details = (
additional_details # To store additional demo info that may not be standard
)
self.is_sac_target = is_sac_target
self.is_sac_sms_target = is_sac_sms_target
self.full_phone_number = full_phone_number
[docs]
@staticmethod
def build_from_str(phone_str: str, phone_type: PhoneType):
"""Takes a string of numbers in several formats and returns a
Phone instance
"""
if not phone_str:
return None
extension = ""
if "x" in phone_str:
phone_str, extension = phone_str.split("x")
phone_num = "".join([x for x in phone_str if x.isdigit()])
return Phone(phone_num[:3], phone_num[3:], phone_type, extension=extension)
def __eq__(self, other):
return vars(self) == vars(other)
def __repr__(self):
return "({}) {}-{} {}({})".format(
self.area_code,
self.phone_number[:3],
self.phone_number[3:],
"x{} ".format(self.extension) if self.extension else "",
self.type,
)
[docs]
class AddressType:
"""Enum for Q2 Address Types"""
RESIDENTIAL = "Residential"
POSTAL = "Postal"
VACATION = "Vacation"
HOME = "Home"
OTHER = "Other"
BUSINESS = "Business"
BILLPAYEE = "BillPayee"
[docs]
class Address(JsonSerializable):
"""Standard Address class so developers don't all roll their own"""
def __init__(
self,
address_1: str,
address_2: str,
city: str,
state: str,
zipcode: str,
address_type: AddressType = AddressType.HOME,
province="",
country: str | None = None,
additional_details: dict | None = None,
address_id: int | None = None,
):
self.address_1 = address_1
self.address_2 = address_2
self.city = city
self.state = state
self.zipcode = zipcode
self.address_type = address_type
self.country = "USA" if not country else country
self.province = province
if not additional_details:
additional_details = {}
self.additional_details = (
additional_details # To store additional demo info that may not be standard
)
self.address_id = address_id
def __eq__(self, other):
return vars(self) == vars(other)
def __repr__(self):
return str(vars(self))
[docs]
@dataclass
class Address3:
"""
Parses city, state, zipcode, and country from address3 line
such as "Austin, TX 94730 USA"
"""
city: str
state: str
zipcode: str
country: str
[docs]
@staticmethod
def from_str(inp: str) -> Address3:
city, remaining = inp.split(",")
state, zipcode, country = remaining.strip().split(" ", 2)
return Address3(city, state, zipcode, country)
[docs]
class DriverLicense(JsonSerializable):
"""Standard DriverLicense class so developers don't all roll their own"""
def __init__(self, dl_number: str, state: str):
if dl_number:
dl_number = "".join(x for x in dl_number if x.isdigit() or x.isalpha())
self.dl_number = dl_number
self.state = state
def __eq__(self, other):
if isinstance(other, DriverLicense):
return vars(self) == vars(other)
return False
def __repr__(self):
return str(vars(self))
[docs]
class DemographicInfo(JsonSerializable):
"""
Smart little class that takes most of the pain out of
'What information do we know about a user?'
Does some data cleansing on input ensuring consumers will all work together.
"""
def __init__(
self,
date_of_birth: str | None,
list_of_emails: list[str] | list[Email],
list_of_phones: list[Phone],
list_of_addresses: list[Address],
first_name: str,
last_name: str,
ssn: str,
mothers_maiden_name="",
middle_name: str = "",
title="",
driver_license: DriverLicense | None = None,
user_info=None,
primary_cif=None,
additional_details: dict | None = None,
user_role_id: int | None = None,
auth_token_serial: str | None = None,
is_admin: bool = False,
email_is_sac_target: bool = None,
):
if date_of_birth:
try:
date_of_birth = parser.parse(date_of_birth).strftime("%m-%d-%Y")
except ValueError:
date_of_birth = None
self.dob = date_of_birth
self.email_objects: list[Email] = []
self.emails: list[str] = self._unpack_emails(list_of_emails)
self.phones: list[Phone] = self._get_uniques(list_of_phones)
self.addresses: list[Address] = self._get_uniques(list_of_addresses)
self.first_name = first_name
self.last_name = last_name
self.middle_name = middle_name
self.title = title
self.mothers_maiden_name = mothers_maiden_name
if ssn:
ssn = "".join(x for x in ssn if x.isdigit())
self.social_security_number = ssn
self.driver_license = driver_license
self.user_info = user_info
self.primary_cif = primary_cif
if not additional_details:
additional_details = {}
self.additional_details = (
additional_details # To store additional demo info that may not be standard
)
# self.user_role_id is used for commercial or treasury users to define the user's role (role must exist
# before user is created)
self.user_role_id = user_role_id
# self.is_admin used for commercial and/or treasury users to indicate the admin user. This will be used to
# enable predefined user properties that apply to a commercial/treasury admin user. See dbo.apispAddUser stored
# proc for more details
self.is_admin = is_admin
# email_is_sac_target must default to None to maintain backwards compatibility
self.email_is_sac_target = email_is_sac_target
# refers to Verisign token (or other vendor). Some FIs have the option to enter a Verisgn token instead
# of the Q2 generated TAC.
self.auth_token_serial = auth_token_serial
@staticmethod
def _get_uniques(full_list: list):
unique_list = []
for item in full_list:
if item and item not in unique_list:
unique_list.append(item)
return unique_list
def __eq__(self, other):
return vars(self) == vars(other)
def __repr__(self):
return str(vars(self))
def _unpack_emails(self, email_list: list[str] | list[Email]):
string_emails = set()
unique_object = []
for obj in email_list:
if isinstance(obj, str):
string_emails.add(obj)
if isinstance(obj, Email) and obj.is_valid_format():
if (
obj.email_address not in unique_object
and obj.email_address not in string_emails
):
self.email_objects.append(obj)
unique_object.append(obj.email_address)
string_emails.add(obj.email_address)
return self._validate_email_format(list(string_emails))
@staticmethod
def _validate_email_format(email_list: list):
return [x for x in email_list if EMAIL_REGEX.match(x)]
[docs]
class Email(JsonSerializable):
def __init__(self, email_address: str, is_sac_target: bool = True) -> None:
self.email_address: str = email_address
self.is_sac_target: bool = is_sac_target
super().__init__()
def __repr__(self):
return (
f"<Email address={self.email_address} is_sac_target={self.is_sac_target} />"
)