Nathan Chapman 98b6f5e9c4 Add tests
2022-05-05 18:17:53 -06:00

227 lines
8.0 KiB
Python

import logging
from decimal import Decimal
from measurement.measures import Weight
from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy
from core.models import Product, OrderLine, Coupon
from core.usps import USPSApiWithRate
from core import (
DiscountValueType,
VoucherType,
TransactionStatus,
OrderStatus,
ShippingMethodType,
ShippingService,
ShippingContainer,
CoffeeGrind
)
from .payments import CreateOrder
logger = logging.getLogger(__name__)
class Cart:
def __init__(self, request):
self.request = request
self.session = request.session
self.coupon_code = self.session.get('coupon_code')
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
def add(self, request, product, quantity=1, grind='', update_quantity=False):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'variations': {},
'price': str(product.price)
}
self.cart[product_id]['variations'][grind] = {'quantity': 0}
if update_quantity:
self.cart[product_id]['variations'][grind]['quantity'] = quantity
else:
if not grind in self.cart[product_id]['variations']:
# create it
self.cart[product_id]['variations'][grind] = {'quantity': quantity}
else:
# add to it
self.cart[product_id]['variations'][grind]['quantity'] += quantity
if len(self) <= 20:
self.save()
else:
messages.warning(request, "Cart is full: 20 items or less.")
def save(self):
self.session[settings.CART_SESSION_ID] = self.cart
self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n')
def remove(self, product):
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
def __iter__(self):
product_ids = self.cart.keys()
products = Product.objects.filter(id__in=product_ids)
for product in products:
self.cart[str(product.id)]['product'] = product
for item in self.cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = Decimal(sum(self.get_item_prices()))
item['quantity'] = self.get_single_item_total_quantity(item)
yield item
def __len__(self):
return sum(self.get_all_item_quantities())
def get_all_item_quantities(self):
for item in self.cart.values():
yield sum([value['quantity'] for value in item['variations'].values()])
def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self):
for item in self.cart.values():
yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])
def get_total_price(self):
return sum(self.get_item_prices())
def get_total_weight(self):
if len(self) > 0:
for item in self:
return item['product'].weight.value * sum(self.get_all_item_quantities())
else:
return 0
def get_shipping_box(self):
if len(self) > 6 and len(self) <= 10:
return ShippingContainer.LG_FLAT_RATE_BOX
elif len(self) > 2 and len(self) <= 6:
return ShippingContainer.REGIONAL_RATE_BOX_B
elif len(self) <= 2:
return ShippingContainer.REGIONAL_RATE_BOX_A
else:
return ShippingContainer.VARIABLE
def get_shipping_cost(self):
if len(self) > 0 and self.session.get("shipping_address"):
try:
usps_rate_request = self.build_usps_rate_request()
except TypeError as e:
return Decimal('0.00')
usps = USPSApiWithRate(settings.USPS_USER_ID, test=True)
validation = usps.get_rate(usps_rate_request)
logger.info(validation.result)
if not 'Error' in validation.result['RateV4Response']['Package']:
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
else:
logger.error("USPS Rate error")
rate = '0.00'
return Decimal(rate)
else:
return Decimal('0.00')
def clear(self):
del self.session[settings.CART_SESSION_ID]
try:
del self.session['coupon_code']
except KeyError:
pass
self.session.modified = True
def build_usps_rate_request(self):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
'pounds': '0',
'ounces': f'{self.get_total_weight()}',
'container': f'{self.get_shipping_box()}',
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}
def build_order_params(self):
return \
{
'items': self,
'total_price': f'{self.get_total_price_after_discount()}',
'item_total': f'{self.get_total_price()}',
'discount': f'{self.get_discount()}',
'shipping_price': f'{self.get_shipping_cost()}',
'tax_total': '0',
'shipping_method': 'US POSTAL SERVICE',
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
}
def create_order(self):
params = self.build_order_params()
logger.info(f'\nParams: {params}\n')
if settings.DEBUG:
response = CreateOrder().create_order(params, debug=True)
else:
response = CreateOrder().create_order(params)
return response
def build_bulk_list(self, order):
bulk_list = []
for item in self:
for key, value in item['variations'].items():
bulk_list.append(OrderLine(
order=order,
product=item['product'],
customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None),
unit_price=item['price'],
quantity=value['quantity'],
tax_rate=2,
))
return bulk_list
def build_shipping_address(self, address):
return \
{
'address_line_1': f'{address["street_address_1"]}',
'address_line_2': f'{address["street_address_2"]}',
'admin_area_2': f'{address["city"]}',
'admin_area_1': f'{address["state"]}',
'postal_code': f'{address["postal_code"]}',
'country_code': 'US'
}
@property
def coupon(self):
if self.coupon_code:
return Coupon.objects.get(code=self.coupon_code)
return None
def get_discount(self):
if self.coupon:
if self.coupon.discount_value_type == DiscountValueType.FIXED:
return round(self.coupon.discount_value, 2)
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
return Decimal('0')
def get_subtotal_price_after_discount(self):
return round(self.get_total_price() - self.get_discount(), 2)
def get_total_price_after_discount(self):
return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2)