Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
lib-py-b2b / address.py
Size: Mime:
import textwrap
from datetime import datetime, timedelta
from pprint import pformat
import re
from lib_b2b.persistent import Persistable
from .errors import InvalidAddressError
from enum import Enum


class AddressEqualityLevel(Enum):
    CITY_STATE_ZIP = 1
    STREET_CITY_STATE_ZIP = 2


class Address:
    @staticmethod
    def from_dict(addr_dict):
        if not addr_dict:
            return None
        return Address(address1=addr_dict.get('address1', None),
                       city=addr_dict.get('city', None),
                       state=addr_dict.get('state', None),
                       postalCode=addr_dict.get('postalCode', None) or addr_dict.get('zip', None),
                       country=addr_dict.get('country', None),
                       phone=addr_dict.get('phone', None),
                       address2=addr_dict.get('address2', None),
                       name=addr_dict.get('name', None),
                       company=addr_dict.get('company', None)
                       )

    def validate(self, customer_edi_id):
        # TODO: - Check to see if we can insert comments in Shopify as a notification mechanism
        if not self.phone:
            raise InvalidAddressError("The order shipping address is missing a phone number and cannot be shipped.")
        if not self.address1:
            raise InvalidAddressError("The order shipping address is missing a street address and cannot be shipped.")
        if not self.city:
            raise InvalidAddressError("The order shipping address is missing a city and cannot be shipped.")
        if not self.state:
            raise InvalidAddressError("The order shipping address is missing a state and cannot be shipped.")
        if not self.postalCode:
            raise InvalidAddressError("The order shipping address is missing a postal code and cannot be shipped.")
        if not self.name and not self.company:
            raise InvalidAddressError("The order shipping address is missing a recipient name or company "
                                      "and cannot be shipped.")
        if self.address1 and len(self.address1) > 35:
            raise InvalidAddressError("The first address line cannot be more than 35 characters.")
        if self.address2 and len(self.address2) > 35:
            raise InvalidAddressError("The second address line cannot be more than 35 characters.")
        if self.city and len(self.city) > 20:
            raise InvalidAddressError("The city cannot be more than 20 characters.")
        if self.state and len(self.state) > 2:
            raise InvalidAddressError("The state cannot be more than 2 characters.")
        if self.name and len(self.name) > 35:
            raise InvalidAddressError("The name cannot be more than 35 characters.")
        if self.company and len(self.company) > 35:
            raise InvalidAddressError("The company name cannot be more than 35 characters.")
        if self.postalCode and not re.search(r'^[0-9]{5}(?:-[0-9]{4})?$', self.postalCode):
            raise InvalidAddressError("Invalid postal code. The postal code doesn't match the standard "
                                      "USPS zip or zip+4 format.")

        from .profile import Profile
        profile = Profile.profile_for(customer=customer_edi_id)
        if profile.fulfillment_config.validate_address:
            from .carrier import CarrierType
            carrier_type = CarrierType(profile.fulfillment_config.carrier_type)
            if carrier_type == CarrierType.FEDEX_API:
                from .fedex_api import FedexAPI
                fapi = FedexAPI(profile)
                validation_result = fapi.validate_address(self)
                return validation_result
            else:
                return None

    def __init__(self, address1, city, state, postalCode, country, phone=None, address2=None, name=None, company=None):
        self.city = city
        self.state = state
        self.postalCode = postalCode
        self.zip = postalCode
        self.country = country
        self.phone = phone
        self.name = name
        self.company = company
        self.address1 = address1
        self.address2 = address2
        self.__wrap_address_lines()

    def __wrap_address_lines(self):
        _address_lines = []
        if not self.address1 and self.address2:
            self.address1 = self.address2
            self.address2 = None

        if self.address1:
            if len(self.address1) > 35:
                _line = self.address1 + ' ' + (self.address2 or '')
                _wrapped = textwrap.wrap(_line, 35, break_long_words=False, max_lines=2)
                _address_lines.extend(_wrapped)
            else:
                _address_lines.append(self.address1)
                _address_lines.append(self.address2)

        self.address1 = _address_lines[0] if _address_lines else None
        self.address2 = _address_lines[1] if _address_lines and len(_address_lines) > 1 else None

    @property
    def postalCode5(self) -> str:
        """
        Get the first five digits of the postal code
        :return: first five zip code
        :rtype: str
        """
        return self.postalCode[:5]

    def merge(self, an_address):
        """
        merges any attributes from the parameter, that are missing in self, into self
        :param an_address: Address
        :return:
        """
        for v in vars(self):
            if not getattr(self, v, None) and getattr(an_address, v, None):
                setattr(self, v, getattr(an_address, v, None))

    def mostly_equals(self, other: 'Address', equality_level: AddressEqualityLevel, ignore_case: bool = True) -> bool:
        """
        Compares the address part excluding names, company, phone, etc
        :param equality_level: The level of the comparison
        :type equality_level: AddressEqualityLevel
        :param other: the address to compare with self
        :type other: Address
        :param ignore_case: ignore case when comparing the strings in the address
        :type ignore_case: bol
        :return: boolean
        :rtype: bool
        """
        if equality_level is AddressEqualityLevel.STREET_CITY_STATE_ZIP:
            self_comparable = f"{self.address1 or ''}{self.address2 or ''}{self.city or ''}{self.state or ''}{self.postalCode5 or ''}"
            other_comparable = f"{other.address1 or ''}{other.address2 or ''}{other.city or ''}{other.state or ''}{other.postalCode5 or ''}"
        elif equality_level is AddressEqualityLevel.CITY_STATE_ZIP:
            self_comparable = f"{self.city or ''}{self.state or ''}{self.postalCode5 or ''}"
            other_comparable = f"{other.city or ''}{other.state or ''}{other.postalCode5 or ''}"
        else:
            raise NotImplementedError

        if ignore_case:
            self_comparable = self_comparable.lower()
            other_comparable = other_comparable.lower()

        return self_comparable == other_comparable

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return vars(self) == vars(other)

    def __repr__(self):
        return "<" + type(self).__name__ + "> " + pformat(vars(self), indent=2, width=1)

    def __str__(self):
        return pformat(vars(self), indent=2, width=1)

    def as_dict(self):
        return vars(self)


class ValidationAddressType(Enum):
    RAW = "RAW"
    NORMALIZED = "NORMALIZED"
    STANDARDIZED = "STANDARDIZED"
    UNKNOWN = "UNKNOWN"


class AddressValidationResult(Persistable):
    # Wrapped this expecting to find future functionality for address validation and error propagation
    def __init__(self, original_address: Address, effective_address: Address, address_type: ValidationAddressType, residential: bool,
                 attributes: dict, estimated_transit_time: int = -1):
        self.original_address = original_address
        self.estimated_transit_time = estimated_transit_time
        self.residential = residential
        self.effective_address = effective_address
        self.attributes = attributes
        self.address_type = address_type

    def is_valid(self):
        resolved = self.attributes.get('Resolved', False)
        pobox = self.attributes.get('POBox', False)
        return self.address_type == ValidationAddressType.STANDARDIZED and resolved and not pobox

    def should_warn(self):
        return self.is_valid() and (self.street_modified or self.city_modified or not self.delivery_point_valid())

    def delivery_point_valid(self):
        return self.attributes.get('DPV', False)

    def is_usable(self):
        return self.is_valid() or (
                self.address_type in [ValidationAddressType.STANDARDIZED, ValidationAddressType.NORMALIZED])

    def is_residential(self):
        return self.residential

    @property
    def street_modified(self):
        return not self.original_address.mostly_equals(self.effective_address, AddressEqualityLevel.STREET_CITY_STATE_ZIP)

    @property
    def city_modified(self):
        return not self.original_address.mostly_equals(self.effective_address, AddressEqualityLevel.CITY_STATE_ZIP)

    def __messages_for_attributes(self):
        _messages = []
        if not self.attributes.get('DPV', False):
            _messages.append("Invalid delivery point. Carrier may not be able to "
                             "deliver to the address as currently entered.")
        if not self.attributes.get('CountrySupported', True):
            _messages.append("Country not supported.")
        if not self.attributes.get('PostalValidated', True):
            _messages.append("Unable to validate the postal code.")
        if not self.attributes.get('CityStateValidated', True):
            _messages.append("City/State are not valid for the postal code.")
        if self.attributes.get('POBox', False):
            _messages.append("Ground delivery is unavailable for a PO Box.")
        if self.attributes.get('SuiteRequiredButMissing', False):
            _messages.append("Missing suite number.")
        if not self.attributes.get('StreetValidated', True):
            _messages.append("Unable to match the street address.")
        if not self.attributes.get('StreetRangeValidated', True):
            _messages.append("Unable to validate the street range.")
        if self.attributes.get('InvalidSuiteNumber', False):
            _messages.append("Invalid suite number.")
        if self.attributes.get('MissingOrAmbiguousDirectional', False):
            _messages.append("Missing or ambiguous directional.")
        if self.attributes.get('MultipleMatches', False):
            _messages.append("Multiple address matches.")
        if self.city_modified:
            _messages.append("City, State, or Postal Code corrected or abbreviated by carrier.")
        if self.street_modified and not self.city_modified:
            _messages.append("Street may have been corrected or abbreviated by the carrier.")
        return _messages

    def get_estimated_delivery(self, ship_date: datetime):
        if self.estimated_transit_time < 0:
            return ship_date
        else:
            return ship_date + timedelta(days=int(self.estimated_transit_time))

    @property
    def messages(self) -> [str]:
        if self.is_valid() and not self.should_warn():
            return ["Address is valid"]
        else:
            return self.__messages_for_attributes() or ["Unable to match address"]

    def as_dict(self):
        return dict(residential=self.residential,
                    original_address=self.original_address.as_dict(),
                    effective_address=self.effective_address.as_dict(),
                    attributes=self.attributes,
                    address_type=self.address_type.value,
                    estimated_transit_time=self.estimated_transit_time)

    @staticmethod
    def from_dict(data: dict):
        return AddressValidationResult(
            original_address=Address.from_dict(data.get('original_address')),
            effective_address=Address.from_dict(data.get('effective_address')),
            address_type=ValidationAddressType(data.get('address_type')),
            residential=data.get('residential'),
            attributes=data.get('attributes'),
            estimated_transit_time=data.get('estimated_transit_time', -1)
        )