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 / shipping.py
Size: Mime:
from decimal import Decimal
from enum import Enum, IntEnum
from typing import Optional

from boto3 import resource
from os import environ
import logging

from botocore.exceptions import ClientError

from lib_b2b.base import BaseClass
from lib_b2b.persistent import Persistable
from .errors import ShipRateNotFound, ShipZoneNotFound, ShippingRateSaveError
from .ship_request import ShipRequest


log_level = logging.getLevelName(environ['LOG_LEVEL']) if 'LOG_LEVEL' in environ else logging.INFO
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
                    datefmt='%d-%m-%Y:%H:%M:%S',
                    level=log_level)
logger = logging.getLogger('lib-b2b-shipping')
fedex_zone_table = environ['fedex_zone_table'] if 'zone_table' in environ else 'prod.b2b.FedexZoneLookup'
ship_rate_table = environ['ship_rate_table'] if 'ship_rate_table' in environ else 'prod.b2b.ShipRates'


class ShippingRateType(Enum):
    FIXED_AMOUNT = 'fixed'
    PERCENTAGE = 'percent'


class ShippingZone(IntEnum):
    ZONE_TWO = 2
    ZONE_THREE = 3
    ZONE_FOUR = 4
    ZONE_FIVE = 5
    ZONE_SIX = 6
    ZONE_SEVEN = 7
    ZONE_EIGHT = 8
    ZONE_NINE = 9

    @staticmethod
    def all() -> ['ShippingZone']:
        return [z for z in ShippingZone]

    @staticmethod
    def for_(postal_code: str):
        if postal_code:
            dynamodb_resource = resource('dynamodb', region_name='us-east-1')
            zone_lookup_table = dynamodb_resource.Table(fedex_zone_table)
            _zip_code = postal_code[:5]
            try:
                response = zone_lookup_table.get_item(Key={'zip_code': _zip_code})
                if 'Item' in response:
                    return ShippingZone(int(response['Item']['zone']))
                else:
                    raise ShipZoneNotFound(f"Unable to find a ship zone for {_zip_code}.")
            except Exception as e:
                raise ShipZoneNotFound(f"Unable to find ship zone for zip: {_zip_code}.") from e
        else:
            return None


class ShippingRate(BaseClass, Persistable):
    def __init__(self, customer_edi_id: str, item: str, zone: ShippingZone, rate: Decimal,
                 shipping_rate_id: str = None, rate_type: ShippingRateType = None) -> None:
        super().__init__()
        if not shipping_rate_id:
            shipping_rate_id = ShippingRate.generate_id(customer_edi_id, item, zone)
        if not rate_type:
            rate_type = ShippingRateType.FIXED_AMOUNT
        self.item = item
        self.shipping_rate_id = shipping_rate_id
        self.customer_edi_id = customer_edi_id
        self.rate_type = rate_type
        self.rate = rate
        self.zone = zone

    @staticmethod
    def generate_id(customer_edi_id, item, zone):
        return f"{customer_edi_id}-{item}-{str(zone.value)}"

    @staticmethod
    def from_dict(data: dict):
        return ShippingRate(
            shipping_rate_id=data.get('id'),
            customer_edi_id=data.get('customer_edi_id'),
            item=data.get('item'),
            zone=ShippingZone(data.get('zone')),
            rate=data.get('rate'),
            rate_type=ShippingRateType.FIXED_AMOUNT
        )

    def as_dict(self) -> dict:
        return {
            'id': self.shipping_rate_id,
            'customer_edi_id': self.customer_edi_id,
            'item': self.item,
            'zone': self.zone.value,
            'rate': self.rate
        }

    @staticmethod
    def fetch(customer_edi_id: str, zone: str, item: str) -> 'ShippingRate':
        if zone is None:
            return None
        dynamodb_resource = resource('dynamodb', region_name='us-east-1')
        rate_lookup_table = dynamodb_resource.Table(ship_rate_table)
        try:
            response = rate_lookup_table.get_item(Key={'id': f"{customer_edi_id}-{item}-{zone}"})
            if 'Item' in response:
                item = response['Item']['rate']
                return ShippingRate.from_dict(item)
            else:
                raise ShipRateNotFound(f"Unable to find a ship rate for customer:{customer_edi_id}, zone:{zone}, item:{item}.")
        except Exception as e:
            raise ShipRateNotFound(f"Unable to find a ship rate for customer:{customer_edi_id}, zone:{zone}, item:{item}.") from e

    @staticmethod
    def fetch_all_rates():
        dynamodb_resource = resource('dynamodb', region_name='us-east-1')
        rate_lookup_table = dynamodb_resource.Table(ship_rate_table)
        try:
            response = rate_lookup_table.scan()
            data = response.get('Items')
            while 'LastEvaluatedKey' in response:
                response = rate_lookup_table.scan(
                    ExclusiveStartKey=response['LastEvaluatedKey']
                )
                data.extend(response.get('Items'))
            return [ShippingRate.from_dict(r) for r in data]
        except ClientError as e:
            raise ShipRateNotFound(e.response['Error']['Message'])
        except Exception as e:
            raise ShipRateNotFound(f"Unable to find a ship rates.") from e

    @staticmethod
    def fetch_rates(customer_edi_id: str, item: str = None):
        dynamodb_resource = resource('dynamodb', region_name='us-east-1')
        rate_lookup_table = dynamodb_resource.Table(ship_rate_table)
        try:
            if item:
                response = rate_lookup_table.query(
                    IndexName='customer_edi_id-item-index',
                    KeyConditionExpression="customer_edi_id = :customer_edi_id and item = :item",
                    ExpressionAttributeValues={
                        ':customer_edi_id': customer_edi_id,
                        ':item': item
                    }
                )
                data = response.get('Items')
                while 'LastEvaluatedKey' in response:
                    response = rate_lookup_table.query(
                        ExclusiveStartKey=response['LastEvaluatedKey'],
                        IndexName='customer_edi_id-index',
                        KeyConditionExpression="customer_edi_id = :customer_edi_id",
                        ExpressionAttributeValues={
                            ':customer_edi_id': customer_edi_id
                        }
                    )
                    data.extend(response.get('Items'))
            else:
                response = rate_lookup_table.query(
                    IndexName='customer_edi_id-index',
                    KeyConditionExpression="customer_edi_id = :customer_edi_id",
                    ExpressionAttributeValues={
                        ':customer_edi_id': customer_edi_id
                    }
                )
                data = response.get('Items')
                while 'LastEvaluatedKey' in response:
                    response = rate_lookup_table.query(
                        ExclusiveStartKey=response['LastEvaluatedKey'],
                        IndexName='customer_edi_id-index',
                        KeyConditionExpression="customer_edi_id = :customer_edi_id",
                        ExpressionAttributeValues={
                            ':customer_edi_id': customer_edi_id
                        }
                    )
                    data.extend(response.get('Items'))
            if data:
                return [ShippingRate.from_dict(r) for r in data]
            else:
                raise ShipRateNotFound(
                    f"Unable to find a ship rates for customer:{customer_edi_id}, item:{item}.")
        except Exception as e:
            raise ShipRateNotFound(
                f"Unable to find a ship rates for customer:{customer_edi_id}, item:{item}.") from e

    def save_or_update(self):
        dynamodb_resource = resource('dynamodb', region_name='us-east-1')
        rate_lookup_table = dynamodb_resource.Table(ship_rate_table)
        try:
            rate_lookup_table.put_item(Item=self.as_dict())
        except ClientError as e:
            raise ShippingRateSaveError(e.response['Error']['Message'])


class Shipping:
    @staticmethod
    def create_shipment(request: ShipRequest):
        return request.ship()

    @staticmethod
    def clean_postal_code(postal_code: str):
        new_postal_code = postal_code
        if len(new_postal_code) > 9 and '-' in new_postal_code:
            new_postal_code = new_postal_code.replace('-', '')
        if len(new_postal_code) > 9:
            new_postal_code = new_postal_code[0:5]
        return new_postal_code

    @staticmethod
    def get_shipping_rate(customer_edi_id: str, postal_code: str, product_id: str) -> Optional[ShippingRate]:
        ship_zone = Shipping.get_ship_zone(postal_code)
        ship_rate = ShippingRate.fetch(customer_edi_id=customer_edi_id,
                                       zone=str(ship_zone.value),
                                       item=product_id)
        if ship_rate:
            return ship_rate
        else:
            return None

    @staticmethod
    def get_ship_zone(zip_code) -> Optional[ShippingZone]:
        return ShippingZone.for_(zip_code)

    @staticmethod
    def get_ship_rates(customer_edi_id, item=None):
        return ShippingRate.fetch_rates(customer_edi_id, item)

    @staticmethod
    def get_ship_rate(customer_edi_id, zone, item):
        return ShippingRate.fetch(customer_edi_id, zone, item).as_dict()