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) try: rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate'] rate = '0.00' except KeyError as e: raise e("USPS Result has no 'Postage'") 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') response = CreateOrder().create_order(params, debug=True) 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)