Add specific shipping choice when 6 bags are in the cart

This commit is contained in:
Nathan Chapman 2022-06-14 10:15:20 -06:00
parent b9b5eb5254
commit 44e73ca790
10 changed files with 191 additions and 78 deletions

View File

@ -2,103 +2,105 @@ from django.conf import settings
class DiscountValueType:
FIXED = "fixed"
PERCENTAGE = "percentage"
FIXED = 'fixed'
PERCENTAGE = 'percentage'
CHOICES = [
(FIXED, settings.DEFAULT_CURRENCY),
(PERCENTAGE, "%"),
(PERCENTAGE, '%'),
]
class VoucherType:
SHIPPING = "shipping"
ENTIRE_ORDER = "entire_order"
SPECIFIC_PRODUCT = "specific_product"
SHIPPING = 'shipping'
ENTIRE_ORDER = 'entire_order'
SPECIFIC_PRODUCT = 'specific_product'
CHOICES = [
(ENTIRE_ORDER, "Entire order"),
(SHIPPING, "Shipping"),
(SPECIFIC_PRODUCT, "Specific products, collections and categories"),
(ENTIRE_ORDER, 'Entire order'),
(SHIPPING, 'Shipping'),
(SPECIFIC_PRODUCT, 'Specific products, collections and categories'),
]
class OrderStatus:
DRAFT = "draft" # fully editable, not finalized order created by staff users
UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled
DRAFT = 'draft' # fully editable, not finalized order created by staff users
UNFULFILLED = 'unfulfilled' # order with no items marked as fulfilled
PARTIALLY_FULFILLED = (
"partially_fulfilled" # order with some items marked as fulfilled
'partially_fulfilled' # order with some items marked as fulfilled
)
FULFILLED = "fulfilled" # order with all items marked as fulfilled
FULFILLED = 'fulfilled' # order with all items marked as fulfilled
PARTIALLY_RETURNED = (
"partially_returned" # order with some items marked as returned
'partially_returned' # order with some items marked as returned
)
RETURNED = "returned" # order with all items marked as returned
CANCELED = "canceled" # permanently canceled order
RETURNED = 'returned' # order with all items marked as returned
CANCELED = 'canceled' # permanently canceled order
CHOICES = [
(DRAFT, "Draft"),
(UNFULFILLED, "Unfulfilled"),
(PARTIALLY_FULFILLED, "Partially fulfilled"),
(PARTIALLY_RETURNED, "Partially returned"),
(RETURNED, "Returned"),
(FULFILLED, "Fulfilled"),
(CANCELED, "Canceled"),
(DRAFT, 'Draft'),
(UNFULFILLED, 'Unfulfilled'),
(PARTIALLY_FULFILLED, 'Partially fulfilled'),
(PARTIALLY_RETURNED, 'Partially returned'),
(RETURNED, 'Returned'),
(FULFILLED, 'Fulfilled'),
(CANCELED, 'Canceled'),
]
class TransactionStatus:
CREATED = "CREATED" # The order was created with the specified context.
SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
APPROVED = "APPROVED" # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
VOIDED = "VOIDED" # All purchase units in the order are voided.
COMPLETED = "COMPLETED" # The payment was authorized or the authorized payment was captured for the order.
PAYER_ACTION_REQUIRED = "PAYER_ACTION_REQUIRED" # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order.
CREATED = 'CREATED' # The order was created with the specified context.
SAVED = 'SAVED' # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
APPROVED = 'APPROVED' # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
VOIDED = 'VOIDED' # All purchase units in the order are voided.
COMPLETED = 'COMPLETED' # The payment was authorized or the authorized payment was captured for the order.
PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED' # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the 'rel':'payer-action' HATEOAS link returned as part of the response prior to authorizing or capturing the order.
CHOICES = [
(CREATED, "Created"),
(SAVED, "Saved"),
(APPROVED, "Approved"),
(VOIDED, "Voided"),
(COMPLETED, "Completed"),
(PAYER_ACTION_REQUIRED, "Payer action required")
(CREATED, 'Created'),
(SAVED, 'Saved'),
(APPROVED, 'Approved'),
(VOIDED, 'Voided'),
(COMPLETED, 'Completed'),
(PAYER_ACTION_REQUIRED, 'Payer action required')
]
class ShippingMethodType:
PRICE_BASED = "price"
WEIGHT_BASED = "weight"
PRICE_BASED = 'price'
WEIGHT_BASED = 'weight'
CHOICES = [
(PRICE_BASED, "Price based shipping"),
(WEIGHT_BASED, "Weight based shipping"),
(PRICE_BASED, 'Price based shipping'),
(WEIGHT_BASED, 'Weight based shipping'),
]
class ShippingService:
FIRST_CLASS = "FIRST CLASS"
PRIORITY = "PRIORITY"
PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL"
FIRST_CLASS = 'FIRST CLASS'
PRIORITY = 'PRIORITY'
PRIORITY_COMMERCIAL = 'PRIORITY COMMERCIAL'
CHOICES = [
(FIRST_CLASS, "First Class"),
(PRIORITY, "Priority"),
(PRIORITY_COMMERCIAL, "Priority Commercial")
(FIRST_CLASS, 'First Class'),
(PRIORITY, 'Priority'),
(PRIORITY_COMMERCIAL, 'Priority Commercial')
]
class ShippingContainer:
LG_FLAT_RATE_BOX = "LG FLAT RATE BOX"
REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA"
REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB"
VARIABLE = "VARIABLE"
LG_FLAT_RATE_BOX = 'LG FLAT RATE BOX'
MD_FLAT_RATE_BOX = 'MD FLAT RATE BOX'
REGIONAL_RATE_BOX_A = 'REGIONALRATEBOXA'
REGIONAL_RATE_BOX_B = 'REGIONALRATEBOXB'
VARIABLE = 'VARIABLE'
CHOICES = [
(LG_FLAT_RATE_BOX, "Flate Rate Box - Large"),
(REGIONAL_RATE_BOX_A, "Regional Rate Box A"),
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
(VARIABLE, "Variable")
(LG_FLAT_RATE_BOX, 'Flate Rate Box - Large'),
(MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'),
(REGIONAL_RATE_BOX_A, 'Regional Rate Box A'),
(REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
(VARIABLE, 'Variable')
]

View File

@ -6,7 +6,7 @@
"name": "Save 10%: Valid",
"code": "MAY2022",
"valid_from": "2022-05-01T06:00:00Z",
"valid_to": "2022-05-31T06:00:00Z",
"valid_to": "2054-05-31T06:00:00Z",
"discount_value_type": "percentage",
"discount_value": "10.00",
"products": [],

View File

@ -43,7 +43,7 @@ class AddressTests(StaticLiveServerTestCase):
state_select.select_by_value('UT')
postal_code_input = self.browser.find_element_by_name('postal_code')
postal_code_input.send_keys('37461')
self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click()
self.browser.find_element_by_xpath('//input[@value="Continue"]').click()
# try:
# WebDriverWait(self.browser, 4).until(
# EC.presence_of_element_located((By.CLASS_NAME, 'errorlist'))
@ -77,7 +77,7 @@ class AddressTests(StaticLiveServerTestCase):
state_select.select_by_value('AK')
postal_code_input = self.browser.find_element_by_name('postal_code')
postal_code_input.send_keys('99801')
self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click()
self.browser.find_element_by_xpath('//input[@value="Continue"]').click()
# try:
# WebDriverWait(self.browser, 4).until(
# EC.presence_of_element_located((By.CLASS_NAME, 'errorlist'))

View File

@ -85,7 +85,7 @@ class CouponTests(StaticLiveServerTestCase):
postal_code_input = self.browser.find_element_by_name('postal_code')
postal_code_input.send_keys('84321')
self.browser.find_element_by_xpath(
'//input[@value="Continue to Payment"]'
'//input[@value="Continue"]'
).click()
self.assertEqual(
@ -140,7 +140,7 @@ class CouponTests(StaticLiveServerTestCase):
postal_code_input = self.browser.find_element_by_name('postal_code')
postal_code_input.send_keys('84321')
self.browser.find_element_by_xpath(
'//input[@value="Continue to Payment"]'
'//input[@value="Continue"]'
).click()
self.assertEqual(

View File

@ -30,6 +30,7 @@ class Cart:
self.request = request
self.session = request.session
self.coupon_code = self.session.get('coupon_code')
self.container = self.session.get('shipping_container')
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
cart = self.session[settings.CART_SESSION_ID] = {}
@ -109,20 +110,26 @@ class Cart:
else:
return 0
def get_shipping_box(self):
def get_shipping_box(self, container=None):
if container:
return container
if self.container:
return self.container
if len(self) > 6 and len(self) <= 10:
return ShippingContainer.LG_FLAT_RATE_BOX
elif len(self) > 2 and len(self) <= 6:
elif len(self) > 3 and len(self) <= 6:
return ShippingContainer.REGIONAL_RATE_BOX_B
elif len(self) <= 2:
elif len(self) <= 3:
return ShippingContainer.REGIONAL_RATE_BOX_A
else:
return ShippingContainer.VARIABLE
def get_shipping_cost(self):
def get_shipping_cost(self, container=None):
if len(self) > 0 and self.session.get("shipping_address"):
try:
usps_rate_request = self.build_usps_rate_request()
usps_rate_request = self.build_usps_rate_request(container)
except TypeError as e:
return Decimal('0.00')
usps = USPSApi(settings.USPS_USER_ID, test=True)
@ -135,7 +142,7 @@ class Cart:
)
logger.info(validation.result)
if not 'Error' in validation.result['RateV4Response']['Package']:
if 'Error' not in validation.result['RateV4Response']['Package']:
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
else:
logger.error("USPS Rate error")
@ -152,7 +159,7 @@ class Cart:
pass
self.session.modified = True
def build_usps_rate_request(self):
def build_usps_rate_request(self, container=None):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
@ -160,7 +167,7 @@ class Cart:
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
'pounds': '0',
'ounces': f'{self.get_total_weight()}',
'container': f'{self.get_shipping_box()}',
'container': f'{self.get_shipping_box(container)}',
'width': '',
'length': '',
'height': '',
@ -168,7 +175,7 @@ class Cart:
'machinable': 'TRUE'
}
def build_order_params(self):
def build_order_params(self, container=None):
return \
{
'items': self,
@ -177,12 +184,14 @@ class Cart:
'discount': f'{self.get_discount()}',
'shipping_price': f'{self.get_shipping_cost()}',
'tax_total': '0',
'shipping_method': 'US POSTAL SERVICE',
'shipping_method': 'US POSTAL SERVICE ' + (
container if container else ''
),
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
}
def create_order(self):
params = self.build_order_params()
def create_order(self, container=None):
params = self.build_order_params(container)
logger.info(f'\nParams: {params}\n')
if settings.DEBUG:
response = CreateOrder().create_order(params, debug=True)

View File

@ -11,7 +11,7 @@ from usps import USPSApi, Address
from captcha.fields import CaptchaField
from core.models import Order
from core import CoffeeGrind
from core import CoffeeGrind, ShippingContainer
from .tasks import contact_form_email
@ -106,6 +106,18 @@ class AddressForm(forms.Form):
)
class CheckoutShippingForm(forms.Form):
SHIPPING_CHOICES = [
(ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'),
(ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
]
shipping_method = forms.ChoiceField(
widget=forms.RadioSelect,
choices=SHIPPING_CHOICES
)
class OrderCreateForm(forms.ModelForm):
email = forms.CharField(widget=forms.HiddenInput())
first_name = forms.CharField(widget=forms.HiddenInput())

View File

@ -14,7 +14,7 @@
{% csrf_token %}
{{form.as_p}}
<p>
<input type="submit" value="Continue to Payment">
<input type="submit" value="Continue">
</p>
</form>
<p>We validate addresses with USPS, if you are having issues please contact us at <a href="mailto:support@ptcoffee.com">support@ptcoffee.com</a> or use the contact information found on our <a href="{% url 'storefront:contact' %}">contact</a> page.</p>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load static %}
{% block head_title %}Checkout | {% endblock %}
{% block content %}
<article>
<header>
<h1>Checkout</h1>
</header>
<section>
<h3>Shipping Method</h3>
<form action="{% url 'storefront:checkout-shipping' %}" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
<fieldset>
<legend>{{ form.shipping_method.label }}</legend>
{% for radio in form.shipping_method %}
<p>
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
{% if 'Flate Rate Box - Medium' in radio.choice_label %}
<strong>${{ MD_FLAT_RATE_BOX }}</strong>
{% elif 'Regional Rate Box B' in radio.choice_label %}
<strong>${{ REGIONAL_RATE_BOX_B }}</strong>
{% endif %}
</label>
{{ radio.tag }}
</p>
{% endfor %}
</fieldset>
<br>
<p>
<input type="submit" value="Continue to Payment">
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -48,6 +48,11 @@ urlpatterns = [
views.CheckoutAddressView.as_view(),
name='checkout-address',
),
path(
'checkout/shipping/',
views.CheckoutShippingView.as_view(),
name='checkout-shipping',
),
path('checkout/', views.OrderCreateView.as_view(), name='order-create'),
path('done/', views.PaymentDoneView.as_view(), name='payment-done'),
path(

View File

@ -32,11 +32,11 @@ from accounts.forms import (
)
from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm
from core import OrderStatus
from core import OrderStatus, ShippingContainer
from .forms import (
AddToCartForm, UpdateCartItemForm, OrderCreateForm,
AddressForm, CouponApplyForm, ContactForm
AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm,
)
from .cart import Cart
from .payments import CaptureOrder
@ -163,7 +163,7 @@ class ProductDetailView(FormMixin, DetailView):
class CheckoutAddressView(FormView):
template_name = 'storefront/checkout_address.html'
form_class = AddressForm
success_url = reverse_lazy('storefront:order-create')
success_url = reverse_lazy('storefront:checkout-shipping')
def get_initial(self):
user = self.request.user
@ -212,6 +212,47 @@ class CheckoutAddressView(FormView):
return super().form_valid(form)
class CheckoutShippingView(FormView):
template_name = 'storefront/checkout_shipping_form.html'
form_class = CheckoutShippingForm
success_url = reverse_lazy('storefront:order-create')
def get(self, request, *args, **kwargs):
cart = Cart(request)
if len(cart) != 6:
if 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
return HttpResponseRedirect(
reverse('storefront:order-create')
)
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect(
reverse('storefront:checkout-address')
)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
cart = Cart(self.request)
context = super().get_context_data(**kwargs)
context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost(
ShippingContainer.MD_FLAT_RATE_BOX
)
context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost(
ShippingContainer.REGIONAL_RATE_BOX_B
)
return context
def form_valid(self, form):
cleaned_data = form.cleaned_data
self.request.session['shipping_container'] = cleaned_data.get(
'shipping_method'
)
return super().form_valid(form)
class OrderCreateView(CreateView):
model = Order
template_name = 'storefront/order_form.html'
@ -219,6 +260,10 @@ class OrderCreateView(CreateView):
success_url = reverse_lazy('storefront:payment-done')
def get(self, request, *args, **kwargs):
cart = Cart(request)
if len(cart) != 6 and 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect(
@ -271,13 +316,14 @@ class OrderCreateView(CreateView):
def form_valid(self, form):
cart = Cart(self.request)
shipping_address = self.request.session.get('shipping_address')
shipping_container = self.request.session.get('shipping_container')
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
form.instance.status = OrderStatus.DRAFT
self.object = form.save()
bulk_list = cart.build_bulk_list(self.object)
objs = OrderLine.objects.bulk_create(bulk_list)
response = cart.create_order()
response = cart.create_order(shipping_container)
data = response.result.__dict__['_dict']
self.request.session['order_id'] = self.object.pk
@ -339,8 +385,8 @@ class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
permission_denied_message = 'Not authorized.'
raise_exception = True
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['order_list'] = Order.objects.without_drafts().filter(
customer=self.object
).prefetch_related('lines')