def test_view_advances_to_payment_form(self, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView can advance to the PaymentForm ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data, follow=True) rendered_html = response.content.decode() self.assertIn('how are you paying?', rendered_html)
def test_view_can_load_and_retrieve_shipping_cost_from_invalid_backend_attribute( self, sum_mock, settings_mock, get_shipping_rate_mock): ''' Test that the CheckoutFormView can recover from a badly-formed package import or missing module. ''' sum_mock.return_value = Decimal(0.00) get_shipping_rate_mock.return_value = Decimal(0.00) settings_mock.FULFILLMENT_BACKENDS = [ ('django.core.mail.backends.locmem.foo', 'Foo') ] session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data, follow=True) self.assertNotIn('shipping', self.client.session[views.UUID])
def test_view_d0es_not_calculate_sales_tax_if_no_nexus_exists( self, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView calculates sales tax when the User does not reside within the nexus of the seller. ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) rate = TaxRate.objects.create(city='Test', state='OK', postal_code='12345', local_tax_rate=Decimal(0.01), state_tax_rate=Decimal(0.043)) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'street_address': 'Buckingham Palace', 'locality': 'London', 'postal_code': 'SW1A 1AA', 'country': 'GB' } response = self.client.post(self.view_url, data=request_data, follow=True) response = response.render() rendered_html = response.content.decode() self.assertNotIn('sales tax', rendered_html)
def test_view_removes_step_data_for_key_in_get_request( self, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView can remove a given key from checkout Session Data when it is specified in an HTTP GET request. ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data, follow=True) request_data = {'shipping': 'shipping'} response = self.client.get(self.view_url, data=request_data) self.assertNotIn('shipping', self.client.session[views.UUID])
def test_view_detects_malformed_session_data(self, calculate_shipping_cost_mock): ''' Test that the View validates Session Data on an ongoing basis. ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. session_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '1234567890', # Simulate bad ZIP code. 'country': 'US' } session = self.client.session session[views.UUID] = {'shipping': session_data} session.save() request_data = {'payment_method_nonce': 'fake-valid-nonce'} response = self.client.post(self.view_url, data=request_data, follow=True) rendered_html = response.content.decode() self.assertNotIn('shipping', self.client.session[views.UUID])
def test_view_can_load_and_retrieve_shipping_cost_from_invalid_backend( self, sum_mock, settings_mock, get_shipping_rate_mock): ''' Test that the CheckoutFormView can successfully load and retrieve a shipping cost from an API. ''' sum_mock.return_value = Decimal(0.00) get_shipping_rate_mock.return_value = Decimal(0.00) settings_mock.FULFILLMENT_BACKENDS = [('foo', 'Foo')] session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data, follow=True) self.assertNotIn('shipping', self.client.session[views.UUID])
def test_view_adds_valid_shipping_information_to_session_variable( self, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView adds valid shipping information from the AddressForm to the User's Session. ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data, follow=True) self.assertEqual(self.client.session[views.UUID]['shipping'], request_data)
def test_view_advances_to_confirmation_page(self, sale_mock, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView can advance to the PaymentForm ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() sale_mock.return_value.is_success = True sale_mock.return_value.transaction.id = '12345' sale_mock.return_value.transaction.amount = Decimal(1.00) sale_mock.return_value.transaction.created_at = timezone.now() sale_mock.return_value.transaction.credit_card = { 'cardholder_name': 'Foo Bar', 'customer_location': 'US', 'card_type': 'Foo', 'last_4': '1234', 'expiration_month': '12', 'expiration_year': '2034', } # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response1 = self.client.post(self.view_url, data=request_data, follow=True) request_data = {'payment_method_nonce': 'fake-valid-nonce'} response2 = self.client.post(self.view_url, data=request_data, follow=True) rendered_html = response2.content.decode() order = Order.objects.last() expected = ('<h2>Order <strong>%s</strong> has been received!</h2>' % order.token) self.assertIn(expected, rendered_html)
def test_view_returns_200_status_if_variants_are_in_cart(self): ''' Test that the CheckoutFormView returns a 200 OK status if there are Variants the SessionCart. ''' session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() response = self.client.get(self.view_url) response = response.render() rendered_html = response.content.decode() self.assertEqual(response.status_code, 200)
def form_valid(self, form): logger.info('%s is valid' % type(form).__name__) cart = SessionCart(self.request.session) variants = Variant.objects.filter(product=self.product) for variant in variants: relevant_attributes = { key: variant.attributes[key] for key in variant.attributes if key in form.cleaned_data } if relevant_attributes == form.cleaned_data: logger.info('User selected "%s"' % variant) cart.add(variant) break return super(ProductView, self).form_valid(form)
def get_context_data(self, **kwargs): context = super(ProductView, self).get_context_data(**kwargs) cart = SessionCart(self.request.session) context.update({'product': self.product, 'cart': cart}) return context
def test_view_gracefully_handles_invalid_step_data_deletion_requests( self, calculate_shipping_cost_mock): ''' Test that the CheckoutFormView gracefully handles step deletion when the step is not present within checkout Session Data. ''' calculate_shipping_cost_mock.return_value = Decimal(1.00) session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP GET request. request_data = {'shipping': 'shipping'} response = self.client.get(self.view_url, data=request_data) self.assertNotIn('shipping', self.client.session[views.UUID])
def test_view_initially_redirects_to_self(self): ''' Test that the CheckoutFormView initially redirects to itself. ''' session = self.client.session cart = SessionCart(session) cart.add(self.variant) session.save() # Set up HTTP POST request. request_data = { 'recipient_name': 'Foo Bar', 'street_address': '123 Test St', 'locality': 'Test', 'region': 'OK', 'postal_code': '12345', 'country': 'US' } response = self.client.post(self.view_url, data=request_data) self.assertEqual(response.url, self.view_url)
def get_context_data(self, **kwargs): context = super(HomePageView, self).get_context_data(**kwargs) cart = SessionCart(self.request.session) context.update({ 'products': [product for product in Product.objects.all() if product.salable], 'cart': cart }) return context
def dispatch(self, request, *args, **kwargs): if not self.request.session.has_key(UUID): self.request.session[UUID] = {} self.cart = SessionCart(self.request.session) self.subtotal = self.cart.total self.shipping_address = self.request.session[UUID].get('shipping') self.shipping_cost = Decimal(0.00) self.sales_tax = Decimal(0.00) self.total = Decimal(0.00) if self.shipping_address: # Calculate the shipping cost. self.shipping_cost = calculate_shipping_cost( address=self.shipping_address, products=self.cart) # Reset 'shipping' step, if no shipping cost could be calculated. if not self.shipping_cost: self.shipping_address = None del self.request.session[UUID]['shipping'] self.request.session.modified = True # Otherwise, calculate sales tax, if the user is based in the US. elif self.shipping_address['country'] == 'US': # Disregard any ZIP+4 information. zip_code = self.shipping_address['postal_code'].split('-')[0] tax_rates = { tax_rate.postal_code: tax_rate.tax_rate for tax_rate in TaxRate.objects.all() } if zip_code in tax_rates: sales_tax_rate = Decimal(0.06) sales_tax = self.subtotal * sales_tax_rate self.sales_tax = Decimal(sales_tax).quantize( Decimal('1.00'), rounding=ROUND_CEILING) self.total = (self.subtotal + self.shipping_cost + self.sales_tax) self.steps = ( { 'name': 'shipping', 'form_class': AddressForm, 'template': 'orders/checkout.html', 'context': { 'description': 'where are we sending this?', } }, { 'name': 'payment', 'form_class': PaymentForm, 'template': 'orders/checkout.html', 'form_kwargs': { 'amount': self.total }, 'context': { 'description': 'how are you paying?', 'client_token': self.client_token, 'shipping_cost': self.shipping_cost, 'sales_tax': self.sales_tax, 'total': self.total } }, ) self.current_step = self.get_current_step() return super(CheckoutFormView, self).dispatch(request, *args, **kwargs)
class CheckoutFormView(FormView): form_class = AddressForm template_name = 'orders/checkout.html' success_url = '/' def __init__(self, *args, **kwargs): super(CheckoutFormView, self).__init__(*args, **kwargs) self.client_token = braintree.ClientToken.generate() self.deleted_data = None def get(self, request, *args, **kwargs): deleted_steps = [ key for key in request.GET if key in self.request.session[UUID] ] for key in deleted_steps: self.delete_previous_step(key) template_response = None if deleted_steps: template_response = self.load_previous_step() return (super(CheckoutFormView, self).get(request, *args, **kwargs) if not template_response else template_response) def post(self, request, *args, **kwargs): template_response = None if not self.session_data_is_valid(): template_response = self.load_previous_step() return (super(CheckoutFormView, self).post(request, *args, **kwargs) if not template_response else template_response) def dispatch(self, request, *args, **kwargs): if not self.request.session.has_key(UUID): self.request.session[UUID] = {} self.cart = SessionCart(self.request.session) self.subtotal = self.cart.total self.shipping_address = self.request.session[UUID].get('shipping') self.shipping_cost = Decimal(0.00) self.sales_tax = Decimal(0.00) self.total = Decimal(0.00) if self.shipping_address: # Calculate the shipping cost. self.shipping_cost = calculate_shipping_cost( address=self.shipping_address, products=self.cart) # Reset 'shipping' step, if no shipping cost could be calculated. if not self.shipping_cost: self.shipping_address = None del self.request.session[UUID]['shipping'] self.request.session.modified = True # Otherwise, calculate sales tax, if the user is based in the US. elif self.shipping_address['country'] == 'US': # Disregard any ZIP+4 information. zip_code = self.shipping_address['postal_code'].split('-')[0] tax_rates = { tax_rate.postal_code: tax_rate.tax_rate for tax_rate in TaxRate.objects.all() } if zip_code in tax_rates: sales_tax_rate = Decimal(0.06) sales_tax = self.subtotal * sales_tax_rate self.sales_tax = Decimal(sales_tax).quantize( Decimal('1.00'), rounding=ROUND_CEILING) self.total = (self.subtotal + self.shipping_cost + self.sales_tax) self.steps = ( { 'name': 'shipping', 'form_class': AddressForm, 'template': 'orders/checkout.html', 'context': { 'description': 'where are we sending this?', } }, { 'name': 'payment', 'form_class': PaymentForm, 'template': 'orders/checkout.html', 'form_kwargs': { 'amount': self.total }, 'context': { 'description': 'how are you paying?', 'client_token': self.client_token, 'shipping_cost': self.shipping_cost, 'sales_tax': self.sales_tax, 'total': self.total } }, ) self.current_step = self.get_current_step() return super(CheckoutFormView, self).dispatch(request, *args, **kwargs) def get_current_step(self): ''' Get the current step within the form wizard. ''' completed_steps = self.request.session.get(UUID, {}) remaining_steps = [ step for step in self.steps if step['name'] not in completed_steps ] current_step = remaining_steps[0] if remaining_steps else None logger.debug('Completed steps: %s' % ', '.join(step for step in completed_steps)) logger.debug('Remaining steps: %s' % ', '.join(step['name'] for step in remaining_steps)) logger.debug('Current step: %s' % (current_step['name'] if current_step else '')) return current_step def get_template_names(self): template_name = self.current_step['template'] return [template_name] def get_form_class(self): logger.debug('Getting form class...') return self.current_step['form_class'] def get_form_kwargs(self): kwargs = super(CheckoutFormView, self).get_form_kwargs() if 'form_kwargs' in self.current_step: kwargs.update(self.current_step['form_kwargs']) return kwargs def get_form(self, form_class=None): ''' Get the Form object that will be supplied to the FormView's context. ''' # Instantiate Form. form = super(CheckoutFormView, self).get_form(form_class=form_class) if isinstance(form, AddressForm): # Determine the IP address associated to the HTTP Request. ip_address = get_real_ip(self.request) # Populate the form's `country` field with the user's apparent # location. if ip_address and not form.is_bound: geo_ip2 = GeoIP2() location = geo_ip2.country(ip_address) form.fields['country'].initial = location['country_code'] logger.debug( 'Got %s %s form' % ('bound' if form.is_bound else 'unbound', form.__class__.__name__)) return form def form_valid(self, form): self.request.session[UUID].update({ self.current_step['name']: form.cleaned_data, }) self.request.session.modified = True # Create an order if there are no more steps to complete. if not self.get_current_step(): shipping_address = Address.objects.create(**self.shipping_address) order = Order.objects.create(shipping_address=shipping_address, subtotal=self.subtotal, sales_tax=self.sales_tax, shipping_cost=self.shipping_cost, total=self.total) self.order_token = order.token for variant in self.cart: purchase = Purchase.objects.create(order=order, variant=variant, price=variant.price) Transaction.objects.create( order=order, transaction_id=form.cleaned_data.get('transaction_id'), amount=form.cleaned_data.get('amount'), cardholder_name=form.cleaned_data.get('cardholder_name'), country=form.cleaned_data.get('country'), payment_card_type=form.cleaned_data.get('payment_card_type'), payment_card_last_4=form.cleaned_data.get( 'payment_card_last_4'), payment_card_expiration_date=form.cleaned_data.get( 'payment_card_expiration_date'), created_at=form.cleaned_data.get('created_at'), origin_ip_address=get_real_ip(self.request), authorized=form.cleaned_data.get('authorized')) return super(CheckoutFormView, self).form_valid(form) def session_data_is_valid(self): session_data = self.request.session.get(UUID, None) is_valid = True if session_data: completed_steps = [ step for step in self.steps if step['name'] in session_data ] for completed_step in completed_steps: step_name = completed_step['name'] form_class = completed_step['form_class'] form = form_class(data=session_data[step_name]) if not form.is_valid(): is_valid = False break return is_valid def delete_previous_step(self, key=None): ''' ''' session_data = self.request.session[UUID] if not key: previous_index = self.steps.index(self.current_step) - 1 key = self.steps[previous_index]['name'] if key in session_data: self.deleted_data = session_data.get(key, None) del session_data[key] self.request.session.modified = True self.current_step = self.get_current_step() def load_previous_step(self): ''' ''' self.delete_previous_step() form_class = self.get_form_class() form = form_class(data=self.deleted_data) if not form.is_valid(): form.add_error(None, 'Something went wrong here...') template_names = self.get_template_names() context_data = self.get_context_data(form=form) template_response = TemplateResponse(self.request, template_names, context_data) return template_response def get_success_url(self): logger.info('Getting Success URL...') if self.get_current_step(): url = reverse('checkout:main') else: # Empty the cart. self.cart.empty() # Delete all CheckoutFormView Session Data. del self.request.session[UUID] # Add the Order Token to CheckoutFormView Session Data. self.request.session[UUID] = {'order_token': self.order_token} url = reverse('checkout:confirmation') logger.info('Redirecting to %s' % url) return url def get_context_data(self, **kwargs): context = super(CheckoutFormView, self).get_context_data(**kwargs) current_position = next(i for (i, step) in enumerate(self.steps) if step['name'] == self.current_step['name']) context.update({ 'cart': self.cart, 'subtotal': self.subtotal, 'current_position': current_position, 'steps': enumerate(self.steps), }) step_context = self.current_step['context'] if step_context: context.update(step_context) return context