kopia lustrzana https://github.com/longclawshop/longclaw
rodzic
c58c7f303e
commit
3de6f48124
|
@ -29,4 +29,5 @@ install:
|
||||||
script: tox -e $TOX_ENV
|
script: tox -e $TOX_ENV
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
|
- coverage xml
|
||||||
- codecov -e TOX_ENV
|
- codecov -e TOX_ENV
|
||||||
|
|
|
@ -5,6 +5,9 @@ Longclaw
|
||||||
.. image:: https://badge.fury.io/py/longclaw.svg
|
.. image:: https://badge.fury.io/py/longclaw.svg
|
||||||
:target: https://badge.fury.io/py/longclaw
|
:target: https://badge.fury.io/py/longclaw
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/gh/JamesRamm/longclaw/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/JamesRamm/longclaw
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/JamesRamm/longclaw.svg?branch=master
|
.. image:: https://travis-ci.org/JamesRamm/longclaw.svg?branch=master
|
||||||
:target: https://travis-ci.org/JamesRamm/longclaw
|
:target: https://travis-ci.org/JamesRamm/longclaw
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
codecov:
|
||||||
|
token: c4e276fe-1d69-49e9-bbbe-fabaf4890222
|
|
@ -2,179 +2,40 @@
|
||||||
|
|
||||||
Checkout
|
Checkout
|
||||||
========
|
========
|
||||||
The Longclaw checkout process consists of three steps:
|
|
||||||
|
|
||||||
- Capturing the payment
|
Longclaw provides a simple, single checkout view.
|
||||||
- Collecting details about the customer - their email and shipping address.
|
The URL for the checkout is ``'checkout/'``.
|
||||||
- Creating a new ``Order`` to store this information.
|
After a successful checkout, it is redirected to ``checkout/success/``.
|
||||||
|
|
||||||
Longclaw currently provides a web API for the checkout process which captures this information in one step.
|
To implement the checkout, simply provide ``'longclawcheckout/checkout.html'`` and
|
||||||
It is left to the user to design the front end template and javascript. In the front end, you should:
|
``'longclawcheckout/success.html'`` templates. (Empty templates will have been created if
|
||||||
|
you ran the longclaw CLI to start your project)
|
||||||
|
|
||||||
- Collect the customer email
|
There are three forms provided in the checkout view:
|
||||||
- Collect the shipping and billing address
|
|
||||||
- Payment capture by tokenizing the payment method (e.g. credit card) or payment itself
|
|
||||||
- (Optionally) calculate the shipping costs
|
|
||||||
- Submit all the information to the server in an AJAX POST request.
|
|
||||||
|
|
||||||
The first two are relatively simple to achieve. Longclaw provides some utilities to help with the rest.
|
:checkout_form:
|
||||||
|
Captures the email address and optionally the shipping option for the checkout.
|
||||||
|
Also captures a boolean indicating whether a different billing address should be used
|
||||||
|
|
||||||
Payment Capture
|
:shipping_form:
|
||||||
===============
|
Captures shipping information.
|
||||||
With Longclaw you can either tokenize the customers payment method (e.g. credit card) and
|
|
||||||
send this to the server for the payment to be captured, or you can use a service such as paypal
|
|
||||||
express checkout, which captures the payment directly and returns a token representing the transaction
|
|
||||||
id. You would then submit this token to your server.
|
|
||||||
|
|
||||||
The second option is often easiest to integrate since the user is redirected to the 3rd party site for payment.
|
:billing_form:
|
||||||
(This is increasingly done via a modal popup rather than a redirect, which makes the user experience smoother).
|
A second address form for capturing alternate billing information. If you do not submit this form
|
||||||
The first option offers tightest integration with the look and feel of your site, but invariable involves more
|
(e.g. by not rendering it on the template), the billing and shipping addresses are assumed to be the same.
|
||||||
front end work and validation.
|
|
||||||
|
|
||||||
Tokenizing the Payment
|
|
||||||
+++++++++++++++++++++++
|
|
||||||
|
|
||||||
To capture the payment with a 3rd party service, you will include some external javascript on your page
|
|
||||||
and often designate a button or ``div`` to initialise the popup/redirect. You will also specify a submit
|
|
||||||
handler to receive the token representing the transaction.
|
|
||||||
|
|
||||||
For example, the braintree javascript client allows express checkout using Paypal. Full details of how
|
|
||||||
to setup are `here <https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3>`_.
|
|
||||||
Other providers such as Stripe offer similar services.
|
|
||||||
|
|
||||||
Once you have received this token, you should submit it, along with the shipping address, billing address,
|
|
||||||
email and shipping rate to the ``api/checkout/prepaid/`` endpoint.
|
|
||||||
|
|
||||||
|
|
||||||
.. note:: The ``api/`` prefix can be configured in your django settings under ``API_URL_PREFIX``.
|
|
||||||
For example, if you want to distinguish the longclaw API from your own, you could set ``API_URL_PREFIX="api/longclaw/"``
|
|
||||||
The checkout url would then be ``api/longclaw/checkout/prepaid/``
|
|
||||||
|
|
||||||
The JSON request data would look like:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
transaction_id: "...",
|
|
||||||
shipping_rate: 0.0,
|
|
||||||
email: "john@smith.com",
|
|
||||||
address: {
|
|
||||||
shipping_name: "john smith",
|
|
||||||
shipping_address_line_1": "...",
|
|
||||||
shipping_address_city: "",
|
|
||||||
shipping_address_zip: "",
|
|
||||||
shipping_address_country: "",
|
|
||||||
billing_name: "john smith",
|
|
||||||
billing_address_line_1: "...",
|
|
||||||
billing_address_city: "",
|
|
||||||
billing_address_zip: "",
|
|
||||||
billing_address_country: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction_id
|
|
||||||
The token returned from e.g. paypal
|
|
||||||
|
|
||||||
When using this method, you do not need to define the ``PAYMENT_GATEWAY`` setting.
|
|
||||||
|
|
||||||
Tokenizing the Payment method
|
|
||||||
+++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
Alternatively, you can pass the payment method for Longclaw to manually capture the payment.
|
|
||||||
Longclaw expects the payment details (i.e. credit card) to be passed as some kind of token in
|
|
||||||
a POST request to ``api/checkout/``.
|
|
||||||
Longclaw will then use the payment gateway defined by the ``PAYMENT_GATEWAY`` setting to capture
|
|
||||||
the payment.
|
|
||||||
To create the initial token representing the customers payment information, you may be able to use
|
|
||||||
the ``api/checkout/token/`` endpoint, passing the card information in the request data. This is dependent
|
|
||||||
upon the backend and it may be preferable to use client javascript libraries provided by your payment
|
|
||||||
gateway (e.g. ``stripe.js`` or ``braintree-web`` ) to generate a token.
|
|
||||||
|
|
||||||
Once the token is generated, the request data to send to ``api/checkout/`` is very similar to that for
|
|
||||||
``api/checkout/prepaid/``:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
token: "...",
|
|
||||||
shipping_rate: 0.0,
|
|
||||||
email: "john@smith.com",
|
|
||||||
address: {
|
|
||||||
shipping_name: "john smith",
|
|
||||||
shipping_address_line_1: "...",
|
|
||||||
shipping_address_city: "",
|
|
||||||
shipping_address_zip: "",
|
|
||||||
shipping_address_country: "",
|
|
||||||
billing_name: "john smith",
|
|
||||||
billing_address_line_1: "...",
|
|
||||||
billing_address_city: "",
|
|
||||||
billing_address_zip: "",
|
|
||||||
billing_address_country: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
token
|
|
||||||
The token for customer details. The key name is dependent on the backend ("token" for stripe, "payment_method_nonce" for braintree)
|
|
||||||
|
|
||||||
shipping_rate
|
|
||||||
Number or string representation of a number (will be cast to float). The shipping costs
|
|
||||||
|
|
||||||
email
|
|
||||||
The customers' email
|
|
||||||
|
|
||||||
.. note:: The ``"token"`` key is dependent upon the payment backend and may be named differently.
|
|
||||||
|
|
||||||
Both ``api/checkout/`` and ``api/checkout/prepaid/`` return a 201 response with ``order_id`` in the JSON data.
|
|
||||||
If the payment fails, ``api/checkout/`` will return a 400 response with ``order_id`` and ``message`` in the JSON data.
|
|
||||||
|
|
||||||
Calculating Shipping Costs
|
|
||||||
==========================
|
|
||||||
|
|
||||||
You will have noticed the need to send ``shipping_rate`` with the checkout. If you are using Longclaws' shipping
|
|
||||||
settings, you can easily calculate the shipping cost either in python or by using the ``api/shipping/cost/`` endpoint.
|
|
||||||
|
|
||||||
Python example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from longclaw.longclawshipping import utils
|
|
||||||
from longclaw.longclawsettings.models import LongclawSettings
|
|
||||||
|
|
||||||
country_code = "GB" # ISO 2-letter country code for a configured shipping rate
|
|
||||||
option = "standard" # Name of shipping rate configured through longclaw admin (only used if more than one shipping rate exists for the given country)
|
|
||||||
|
|
||||||
settings = LongclawSettings.for_site(request.site)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = utils.get_shipping_cost(country_code, option, settings)
|
|
||||||
except InvalidShippingRate:
|
|
||||||
# More than 1 shipping rate for the country exists,
|
|
||||||
# but the supplied option doesnt match any
|
|
||||||
pass
|
|
||||||
except InvalidShippingCountry:
|
|
||||||
# A shipping rate for this country does not exist and ``default_shipping_enabled``
|
|
||||||
# is set to ``False`` in the longclaw admin settings
|
|
||||||
|
|
||||||
Javascript example:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
fetch(
|
|
||||||
"api/shipping/cost/",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json, application/json, application/coreapi+json',
|
|
||||||
"Content-Type": 'application/json"
|
|
||||||
},
|
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({
|
|
||||||
country_code: "GB",
|
|
||||||
shipping_rate_name: "standard"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
).then(response => {...})
|
|
||||||
|
|
||||||
|
Generally, you may need to use a little javascript to optionally render the form if the user selects
|
||||||
|
'different billing address'.
|
||||||
|
|
||||||
|
Payment forms
|
||||||
|
-------------
|
||||||
|
|
||||||
|
It is up to you to render a payment form and then pass the token in the POST data.
|
||||||
|
Normally, the payment gateway chosen will have a javascript integration to render a form for you
|
||||||
|
and tokenize the payment method (e.g. braintrees 'hosted fields')
|
||||||
|
|
||||||
|
Longclaws' payment gateways provide some helpful utilities to load client javascript and generate tokens.
|
||||||
|
Loading ``longclawcheckout_tags`` in your template will allow you to retrieve the gateways' javascript libraries
|
||||||
|
as script tags (``{% gateway_client_js %}`` and generate a client token (``{% gateway_token %}``).
|
||||||
|
A little javascript is then required to setup your form and ask the gateway to tokenize the payment method for you.
|
||||||
|
You should then add this token to the request POST data (e.g. with a hidden input field).
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
.. checkout:
|
||||||
|
|
||||||
|
The Checkout API
|
||||||
|
================
|
||||||
|
The checkout API allows you to create more complex javascript-based checkout flows by providing some
|
||||||
|
simple endpoints for capturing payments and orders.
|
||||||
|
In the front end, you should:
|
||||||
|
|
||||||
|
- Collect the customer email
|
||||||
|
- Collect the shipping and billing address
|
||||||
|
- Payment capture by tokenizing the payment method (e.g. credit card) or payment itself
|
||||||
|
- (Optionally) calculate the shipping costs
|
||||||
|
- Submit all the information to the server in an AJAX POST request.
|
||||||
|
|
||||||
|
The first two are relatively simple to achieve. Longclaw provides some utilities to help with the rest.
|
||||||
|
|
||||||
|
Payment Capture
|
||||||
|
===============
|
||||||
|
With Longclaw you can either tokenize the customers payment method (e.g. credit card) and
|
||||||
|
send this to the server for the payment to be captured, or you can use a service such as paypal
|
||||||
|
express checkout, which captures the payment directly and returns a token representing the transaction
|
||||||
|
id. You would then submit this token to your server.
|
||||||
|
|
||||||
|
The second option is often easiest to integrate since the user is redirected to the 3rd party site for payment.
|
||||||
|
(This is increasingly done via a modal popup rather than a redirect, which makes the user experience smoother).
|
||||||
|
The first option offers tightest integration with the look and feel of your site, but invariable involves more
|
||||||
|
front end work and validation.
|
||||||
|
|
||||||
|
Tokenizing the Payment
|
||||||
|
+++++++++++++++++++++++
|
||||||
|
|
||||||
|
To capture the payment with a 3rd party service, you will include some external javascript on your page
|
||||||
|
and often designate a button or ``div`` to initialise the popup/redirect. You will also specify a submit
|
||||||
|
handler to receive the token representing the transaction.
|
||||||
|
|
||||||
|
For example, the braintree javascript client allows express checkout using Paypal. Full details of how
|
||||||
|
to setup are `here <https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3>`_.
|
||||||
|
Other providers such as Stripe offer similar services.
|
||||||
|
|
||||||
|
Once you have received this token, you should submit it, along with the shipping address, billing address,
|
||||||
|
email and shipping rate to the ``api/checkout/prepaid/`` endpoint.
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: The ``api/`` prefix can be configured in your django settings under ``API_URL_PREFIX``.
|
||||||
|
For example, if you want to distinguish the longclaw API from your own, you could set ``API_URL_PREFIX="api/longclaw/"``
|
||||||
|
The checkout url would then be ``api/longclaw/checkout/prepaid/``
|
||||||
|
|
||||||
|
The JSON request data would look like:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
transaction_id: "...",
|
||||||
|
shipping_rate: 0.0,
|
||||||
|
email: "john@smith.com",
|
||||||
|
address: {
|
||||||
|
shipping_name: "john smith",
|
||||||
|
shipping_address_line_1": "...",
|
||||||
|
shipping_address_city: "",
|
||||||
|
shipping_address_zip: "",
|
||||||
|
shipping_address_country: "",
|
||||||
|
billing_name: "john smith",
|
||||||
|
billing_address_line_1: "...",
|
||||||
|
billing_address_city: "",
|
||||||
|
billing_address_zip: "",
|
||||||
|
billing_address_country: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction_id
|
||||||
|
The token returned from e.g. paypal
|
||||||
|
|
||||||
|
When using this method, you do not need to define the ``PAYMENT_GATEWAY`` setting.
|
||||||
|
|
||||||
|
Tokenizing the Payment method
|
||||||
|
+++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
Alternatively, you can pass the payment method for Longclaw to manually capture the payment.
|
||||||
|
Longclaw expects the payment details (i.e. credit card) to be passed as some kind of token in
|
||||||
|
a POST request to ``api/checkout/``.
|
||||||
|
Longclaw will then use the payment gateway defined by the ``PAYMENT_GATEWAY`` setting to capture
|
||||||
|
the payment.
|
||||||
|
To create the initial token representing the customers payment information, you may be able to use
|
||||||
|
the ``api/checkout/token/`` endpoint, passing the card information in the request data. This is dependent
|
||||||
|
upon the backend and it may be preferable to use client javascript libraries provided by your payment
|
||||||
|
gateway (e.g. ``stripe.js`` or ``braintree-web`` ) to generate a token.
|
||||||
|
|
||||||
|
Once the token is generated, the request data to send to ``api/checkout/`` is very similar to that for
|
||||||
|
``api/checkout/prepaid/``:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
token: "...",
|
||||||
|
shipping_rate: 0.0,
|
||||||
|
email: "john@smith.com",
|
||||||
|
address: {
|
||||||
|
shipping_name: "john smith",
|
||||||
|
shipping_address_line_1: "...",
|
||||||
|
shipping_address_city: "",
|
||||||
|
shipping_address_zip: "",
|
||||||
|
shipping_address_country: "",
|
||||||
|
billing_name: "john smith",
|
||||||
|
billing_address_line_1: "...",
|
||||||
|
billing_address_city: "",
|
||||||
|
billing_address_zip: "",
|
||||||
|
billing_address_country: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token
|
||||||
|
The token for customer details. The key name is dependent on the backend ("token" for stripe, "payment_method_nonce" for braintree)
|
||||||
|
|
||||||
|
shipping_rate
|
||||||
|
Number or string representation of a number (will be cast to float). The shipping costs
|
||||||
|
|
||||||
|
email
|
||||||
|
The customers' email
|
||||||
|
|
||||||
|
.. note:: The ``"token"`` key is dependent upon the payment backend and may be named differently.
|
||||||
|
|
||||||
|
Both ``api/checkout/`` and ``api/checkout/prepaid/`` return a 201 response with ``order_id`` in the JSON data.
|
||||||
|
If the payment fails, ``api/checkout/`` will return a 400 response with ``order_id`` and ``message`` in the JSON data.
|
||||||
|
|
||||||
|
Calculating Shipping Costs
|
||||||
|
==========================
|
||||||
|
|
||||||
|
You will have noticed the need to send ``shipping_rate`` with the checkout. If you are using Longclaws' shipping
|
||||||
|
settings, you can easily calculate the shipping cost either in python or by using the ``api/shipping/cost/`` endpoint.
|
||||||
|
|
||||||
|
Python example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from longclaw.longclawshipping import utils
|
||||||
|
from longclaw.longclawsettings.models import LongclawSettings
|
||||||
|
|
||||||
|
country_code = "GB" # ISO 2-letter country code for a configured shipping rate
|
||||||
|
option = "standard" # Name of shipping rate configured through longclaw admin (only used if more than one shipping rate exists for the given country)
|
||||||
|
|
||||||
|
settings = LongclawSettings.for_site(request.site)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = utils.get_shipping_cost(country_code, option, settings)
|
||||||
|
except InvalidShippingRate:
|
||||||
|
# More than 1 shipping rate for the country exists,
|
||||||
|
# but the supplied option doesnt match any
|
||||||
|
pass
|
||||||
|
except InvalidShippingCountry:
|
||||||
|
# A shipping rate for this country does not exist and ``default_shipping_enabled``
|
||||||
|
# is set to ``False`` in the longclaw admin settings
|
||||||
|
|
||||||
|
Javascript example:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
"api/shipping/cost/",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, application/json, application/coreapi+json',
|
||||||
|
"Content-Type": 'application/json"
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
country_code: "GB",
|
||||||
|
shipping_rate_name: "standard"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).then(response => {...})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ Usage Guide
|
||||||
products
|
products
|
||||||
basket
|
basket
|
||||||
checkout
|
checkout
|
||||||
|
checkout_api
|
||||||
shipping
|
shipping
|
||||||
orders
|
orders
|
||||||
payments
|
payments
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django import template
|
||||||
|
from longclaw.longclawbasket.utils import get_basket_items
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def basket(context):
|
||||||
|
'''
|
||||||
|
Return the BasketItems in the current basket
|
||||||
|
'''
|
||||||
|
items, _ = get_basket_items(context["request"])
|
||||||
|
return items
|
|
@ -2,16 +2,13 @@
|
||||||
Shipping logic and payment capture API
|
Shipping logic and payment capture API
|
||||||
'''
|
'''
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework import permissions, status
|
from rest_framework import permissions, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from longclaw.longclawbasket.utils import get_basket_items, destroy_basket
|
from longclaw.longclawbasket.utils import destroy_basket
|
||||||
from longclaw.longclawcheckout.utils import PaymentError, create_order
|
from longclaw.longclawcheckout.utils import create_order, GATEWAY
|
||||||
from longclaw import settings
|
from longclaw.longclawcheckout.errors import PaymentError
|
||||||
|
|
||||||
gateway = import_string(settings.PAYMENT_GATEWAY)()
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([permissions.AllowAny])
|
@permission_classes([permissions.AllowAny])
|
||||||
|
@ -20,7 +17,7 @@ def create_token(request):
|
||||||
payment backend. Some payment backends (e.g. braintree) support creating a payment
|
payment backend. Some payment backends (e.g. braintree) support creating a payment
|
||||||
token, which should be imported from the backend as 'get_token'
|
token, which should be imported from the backend as 'get_token'
|
||||||
'''
|
'''
|
||||||
token = gateway.get_token(request)
|
token = GATEWAY.get_token(request)
|
||||||
return Response({'token': token}, status=status.HTTP_200_OK)
|
return Response({'token': token}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -36,23 +33,19 @@ def create_order_with_token(request):
|
||||||
# Get the request data
|
# Get the request data
|
||||||
try:
|
try:
|
||||||
address = request.data['address']
|
address = request.data['address']
|
||||||
postage = float(request.data['shipping_rate'])
|
shipping_option = request.data.get('shipping_option', None)
|
||||||
email = request.data['email']
|
email = request.data['email']
|
||||||
transaction_id = request.data['transaction_id']
|
transaction_id = request.data['transaction_id']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return Response(data={"message": "Missing parameters from request data"},
|
return Response(data={"message": "Missing parameters from request data"},
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Get the contents of the basket
|
|
||||||
items, _ = get_basket_items(request)
|
|
||||||
# Create the order
|
# Create the order
|
||||||
ip_address = request.data.get('ip', '0.0.0.0')
|
|
||||||
order = create_order(
|
order = create_order(
|
||||||
items,
|
|
||||||
address,
|
|
||||||
email,
|
email,
|
||||||
postage,
|
request,
|
||||||
ip_address
|
addresses=address,
|
||||||
|
shipping_option=shipping_option
|
||||||
)
|
)
|
||||||
|
|
||||||
order.payment_date = timezone.now()
|
order.payment_date = timezone.now()
|
||||||
|
@ -85,41 +78,22 @@ def capture_payment(request):
|
||||||
billing_address_country
|
billing_address_country
|
||||||
|
|
||||||
'email': Email address of the customer
|
'email': Email address of the customer
|
||||||
'ip': IP address of the customer
|
|
||||||
'shipping': The shipping rate (in the sites' currency)
|
'shipping': The shipping rate (in the sites' currency)
|
||||||
'''
|
'''
|
||||||
|
# get request data
|
||||||
# Get the contents of the basket
|
|
||||||
items, _ = get_basket_items(request)
|
|
||||||
|
|
||||||
# Compute basket total
|
|
||||||
total = 0
|
|
||||||
for item in items:
|
|
||||||
total += item.total()
|
|
||||||
|
|
||||||
# Create the order
|
|
||||||
address = request.data['address']
|
address = request.data['address']
|
||||||
postage = float(request.data['shipping_rate'])
|
email = request.data.get('email', None)
|
||||||
email = request.data['email']
|
shipping_option = request.data.get('shipping_option', None)
|
||||||
ip_address = request.data.get('ip', '0.0.0.0')
|
|
||||||
order = create_order(
|
|
||||||
items,
|
|
||||||
address,
|
|
||||||
email,
|
|
||||||
postage,
|
|
||||||
ip_address
|
|
||||||
)
|
|
||||||
|
|
||||||
# Capture the payment
|
# Capture the payment
|
||||||
try:
|
try:
|
||||||
desc = 'Payment from {} for order id #{}'.format(request.data['email'], order.id)
|
order = create_order(
|
||||||
transaction_id = gateway.create_payment(request,
|
email,
|
||||||
float(total) + postage,
|
request,
|
||||||
description=desc)
|
addresses=address,
|
||||||
order.payment_date = timezone.now()
|
shipping_option=shipping_option,
|
||||||
order.transaction_id = transaction_id
|
capture_payment=True
|
||||||
# Once the order has been successfully taken, we can empty the basket
|
)
|
||||||
destroy_basket(request)
|
|
||||||
response = Response(data={"order_id": order.id},
|
response = Response(data={"order_id": order.id},
|
||||||
status=status.HTTP_201_CREATED)
|
status=status.HTTP_201_CREATED)
|
||||||
except PaymentError as err:
|
except PaymentError as err:
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
class PaymentError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = str(message)
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
class CheckoutForm(forms.Form):
|
||||||
|
'''
|
||||||
|
Captures extra info required for checkout
|
||||||
|
'''
|
||||||
|
email = forms.EmailField()
|
||||||
|
shipping_option = forms.ChoiceField(required=False)
|
||||||
|
different_billing_address = forms.BooleanField(required=False)
|
|
@ -1,6 +1,6 @@
|
||||||
from longclaw.longclawcheckout.utils import PaymentError
|
from longclaw.longclawcheckout.errors import PaymentError
|
||||||
|
|
||||||
class BasePayment():
|
class BasePayment(object):
|
||||||
'''
|
'''
|
||||||
Provides the interface for payment backends and
|
Provides the interface for payment backends and
|
||||||
can function as a dummy backend for testing.
|
can function as a dummy backend for testing.
|
||||||
|
@ -13,20 +13,28 @@ class BasePayment():
|
||||||
Can be used for testing - to simulate a failed payment/error,
|
Can be used for testing - to simulate a failed payment/error,
|
||||||
pass `error: true` in the request data.
|
pass `error: true` in the request data.
|
||||||
'''
|
'''
|
||||||
err = request.data.get("error", False)
|
err = request.POST.get("error", False)
|
||||||
if err:
|
if err:
|
||||||
raise PaymentError("Dummy error requested")
|
raise PaymentError("Dummy error requested")
|
||||||
|
|
||||||
return 'fake_transaction_id'
|
return 'fake_transaction_id'
|
||||||
|
|
||||||
def get_token(self, request):
|
def get_token(self, request=None):
|
||||||
'''
|
'''
|
||||||
Dummy function for generating a client token through
|
Dummy function for generating a client token through
|
||||||
a payment gateway. Most (all?) gateways have a flow which
|
a payment gateway. Most (all?) gateways have a flow which
|
||||||
involves requesting a token from the server (usually to
|
involves requesting a token from the server to initialise
|
||||||
tokenize the payment method) and then passing that token
|
a client.
|
||||||
to another api endpoint to create the payment.
|
|
||||||
|
|
||||||
This function should be overriden in child classes
|
This function should be overriden in child classes
|
||||||
'''
|
'''
|
||||||
return 'dummy_token'
|
return 'dummy_token'
|
||||||
|
|
||||||
|
def client_js(self):
|
||||||
|
'''
|
||||||
|
Return any client javascript library paths required
|
||||||
|
by the payment integration.
|
||||||
|
Should return an iterable of JS paths which can
|
||||||
|
be used in <script> tags
|
||||||
|
'''
|
||||||
|
return ('http://dummy.js', 'dummy.js')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import braintree
|
import braintree
|
||||||
from longclaw import settings
|
from longclaw import settings
|
||||||
from longclaw.longclawsettings.models import LongclawSettings
|
from longclaw.longclawsettings.models import LongclawSettings
|
||||||
from longclaw.longclawcheckout.utils import PaymentError
|
from longclaw.longclawcheckout.errors import PaymentError
|
||||||
from longclaw.longclawcheckout.gateways import BasePayment
|
from longclaw.longclawcheckout.gateways import BasePayment
|
||||||
|
|
||||||
class BraintreePayment(BasePayment):
|
class BraintreePayment(BasePayment):
|
||||||
|
@ -9,13 +9,17 @@ class BraintreePayment(BasePayment):
|
||||||
Create a payment using Braintree
|
Create a payment using Braintree
|
||||||
'''
|
'''
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
braintree.Configuration.configure(braintree.Environment.Sandbox,
|
if settings.BRAINTREE_SANDBOX:
|
||||||
|
env = braintree.Environment.Sandbox
|
||||||
|
else:
|
||||||
|
env = braintree.Environment.Production
|
||||||
|
braintree.Configuration.configure(env,
|
||||||
merchant_id=settings.BRAINTREE_MERCHANT_ID,
|
merchant_id=settings.BRAINTREE_MERCHANT_ID,
|
||||||
public_key=settings.BRAINTREE_PUBLIC_KEY,
|
public_key=settings.BRAINTREE_PUBLIC_KEY,
|
||||||
private_key=settings.BRAINTREE_PRIVATE_KEY)
|
private_key=settings.BRAINTREE_PRIVATE_KEY)
|
||||||
|
|
||||||
def create_payment(self, request, amount):
|
def create_payment(self, request, amount, description=''):
|
||||||
nonce = request.data['payment_method_nonce']
|
nonce = request.POST.get('payment_method_nonce')
|
||||||
result = braintree.Transaction.sale({
|
result = braintree.Transaction.sale({
|
||||||
"amount": str(amount),
|
"amount": str(amount),
|
||||||
"payment_method_nonce": nonce,
|
"payment_method_nonce": nonce,
|
||||||
|
@ -27,9 +31,15 @@ class BraintreePayment(BasePayment):
|
||||||
raise PaymentError(result)
|
raise PaymentError(result)
|
||||||
return result.transaction.id
|
return result.transaction.id
|
||||||
|
|
||||||
def get_token(self, request):
|
def get_token(self, request=None):
|
||||||
# Generate client token for the dropin ui
|
# Generate client token
|
||||||
return braintree.ClientToken.generate({})
|
return braintree.ClientToken.generate()
|
||||||
|
|
||||||
|
def client_js(self):
|
||||||
|
return (
|
||||||
|
"https://js.braintreegateway.com/web/3.15.0/js/client.min.js",
|
||||||
|
"https://js.braintreegateway.com/web/3.15.0/js/hosted-fields.min.js"
|
||||||
|
)
|
||||||
|
|
||||||
class PaypalVZeroPayment(BasePayment):
|
class PaypalVZeroPayment(BasePayment):
|
||||||
'''
|
'''
|
||||||
|
@ -40,7 +50,7 @@ class PaypalVZeroPayment(BasePayment):
|
||||||
|
|
||||||
def create_payment(self, request, amount, description=''):
|
def create_payment(self, request, amount, description=''):
|
||||||
longclaw_settings = LongclawSettings.for_site(request.site)
|
longclaw_settings = LongclawSettings.for_site(request.site)
|
||||||
nonce = request.data['payment_method_nonce']
|
nonce = request.POST.get('payment_method_nonce')
|
||||||
result = self.gateway.transaction.sale({
|
result = self.gateway.transaction.sale({
|
||||||
"amount": str(amount),
|
"amount": str(amount),
|
||||||
"payment_method_nonce": nonce,
|
"payment_method_nonce": nonce,
|
||||||
|
@ -57,3 +67,10 @@ class PaypalVZeroPayment(BasePayment):
|
||||||
|
|
||||||
def get_token(self, request):
|
def get_token(self, request):
|
||||||
return self.gateway.client_token.generate()
|
return self.gateway.client_token.generate()
|
||||||
|
|
||||||
|
def client_js(self):
|
||||||
|
return (
|
||||||
|
"https://www.paypalobjects.com/api/checkout.js",
|
||||||
|
"https://js.braintreegateway.com/web/3.15.0/js/client.min.js",
|
||||||
|
"https://js.braintreegateway.com/web/3.15.0/js/paypal-checkout.min.js"
|
||||||
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import math
|
||||||
import stripe
|
import stripe
|
||||||
from longclaw.settings import STRIPE_SECRET
|
from longclaw.settings import STRIPE_SECRET
|
||||||
from longclaw.longclawsettings.models import LongclawSettings
|
from longclaw.longclawsettings.models import LongclawSettings
|
||||||
from longclaw.longclawcheckout.utils import PaymentError
|
from longclaw.longclawcheckout.errors import PaymentError
|
||||||
from longclaw.longclawcheckout.gateways import BasePayment
|
from longclaw.longclawcheckout.gateways import BasePayment
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,14 +13,14 @@ class StripePayment(BasePayment):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
stripe.api_key = STRIPE_SECRET
|
stripe.api_key = STRIPE_SECRET
|
||||||
|
|
||||||
def create_payment(self, request, amount):
|
def create_payment(self, request, amount, description=''):
|
||||||
try:
|
try:
|
||||||
currency = LongclawSettings.for_site(request.site).currency
|
currency = LongclawSettings.for_site(request.site).currency
|
||||||
charge = stripe.Charge.create(
|
charge = stripe.Charge.create(
|
||||||
amount=int(math.ceil(amount * 100)), # Amount in pence
|
amount=int(math.ceil(amount * 100)), # Amount in pence
|
||||||
currency=currency.lower(),
|
currency=currency.lower(),
|
||||||
source=request.data['token'],
|
source=request.data['token'],
|
||||||
description="Payment from"
|
description=description
|
||||||
)
|
)
|
||||||
return charge.id
|
return charge.id
|
||||||
except stripe.error.CardError as error:
|
except stripe.error.CardError as error:
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
from django import template
|
||||||
|
from longclaw.longclawcheckout.utils import GATEWAY
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def gateway_client_js():
|
||||||
|
'''
|
||||||
|
Template tag which provides a `script` tag for each javascript item
|
||||||
|
required by the payment gateway
|
||||||
|
'''
|
||||||
|
javascripts = GATEWAY.client_js()
|
||||||
|
if isinstance(javascripts, (tuple, list)):
|
||||||
|
tags = []
|
||||||
|
for js in javascripts:
|
||||||
|
tags.append('<script type="text/javascript" src="{}"></script>'.format(js))
|
||||||
|
return tags
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'function client_js of {} must return a list or tuple'.format(GATEWAY.__name__))
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def gateway_token():
|
||||||
|
'''
|
||||||
|
Provide a client token from the chosen gateway
|
||||||
|
'''
|
||||||
|
return GATEWAY.get_token()
|
|
@ -1,12 +1,22 @@
|
||||||
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from longclaw.tests.utils import LongclawTestCase, BasketItemFactory
|
from django.contrib.sites.models import Site
|
||||||
|
try:
|
||||||
|
from django.urls import reverse
|
||||||
|
except ImportError:
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from longclaw.tests.utils import LongclawTestCase, AddressFactory, BasketItemFactory, CountryFactory, OrderFactory
|
||||||
from longclaw.longclawcheckout.utils import create_order
|
from longclaw.longclawcheckout.utils import create_order
|
||||||
|
from longclaw.longclawcheckout.forms import CheckoutForm
|
||||||
|
from longclaw.longclawcheckout.views import CheckoutView
|
||||||
from longclaw.longclawbasket.utils import basket_id
|
from longclaw.longclawbasket.utils import basket_id
|
||||||
|
|
||||||
|
|
||||||
class CheckoutTest(LongclawTestCase):
|
class CheckoutApiTest(LongclawTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
self.addresses = {
|
self.addresses = {
|
||||||
'shipping_name': '',
|
'shipping_name': '',
|
||||||
'shipping_address_line1': '',
|
'shipping_address_line1': '',
|
||||||
|
@ -20,16 +30,14 @@ class CheckoutTest(LongclawTestCase):
|
||||||
'billing_address_country': ''
|
'billing_address_country': ''
|
||||||
}
|
}
|
||||||
self.email = "test@test.com"
|
self.email = "test@test.com"
|
||||||
request = RequestFactory().get('/')
|
self.request = RequestFactory().get('/')
|
||||||
request.session = {}
|
self.request.session = {}
|
||||||
self.basket_id = basket_id(request)
|
self.basket_id = basket_id(self.request)
|
||||||
|
|
||||||
def test_create_order(self):
|
def test_create_order(self):
|
||||||
shipping_rate = 0
|
BasketItemFactory(basket_id=self.basket_id),
|
||||||
basket_items = [BasketItemFactory(basket_id=self.basket_id),
|
BasketItemFactory(basket_id=self.basket_id)
|
||||||
BasketItemFactory(basket_id=self.basket_id)]
|
order = create_order(self.email, self.request, self.addresses)
|
||||||
order = create_order(basket_items, self.addresses,
|
|
||||||
self.email, shipping_rate)
|
|
||||||
self.assertIsNotNone(order)
|
self.assertIsNotNone(order)
|
||||||
self.assertEqual(self.email, order.email)
|
self.assertEqual(self.email, order.email)
|
||||||
self.assertEqual(order.items.count(), 2)
|
self.assertEqual(order.items.count(), 2)
|
||||||
|
@ -42,8 +50,7 @@ class CheckoutTest(LongclawTestCase):
|
||||||
BasketItemFactory(basket_id=self.basket_id)
|
BasketItemFactory(basket_id=self.basket_id)
|
||||||
data = {
|
data = {
|
||||||
'address': self.addresses,
|
'address': self.addresses,
|
||||||
'email': self.email,
|
'email': self.email
|
||||||
'shipping_rate': 0
|
|
||||||
}
|
}
|
||||||
self.post_test(data, 'longclaw_checkout', format='json')
|
self.post_test(data, 'longclaw_checkout', format='json')
|
||||||
|
|
||||||
|
@ -55,7 +62,6 @@ class CheckoutTest(LongclawTestCase):
|
||||||
data = {
|
data = {
|
||||||
'address': self.addresses,
|
'address': self.addresses,
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
'shipping_rate': 3.95,
|
|
||||||
'transaction_id': 'blahblah'
|
'transaction_id': 'blahblah'
|
||||||
}
|
}
|
||||||
self.post_test(data, 'longclaw_checkout_prepaid', format='json')
|
self.post_test(data, 'longclaw_checkout_prepaid', format='json')
|
||||||
|
@ -65,3 +71,111 @@ class CheckoutTest(LongclawTestCase):
|
||||||
Test api endpoint checkout/token/
|
Test api endpoint checkout/token/
|
||||||
"""
|
"""
|
||||||
self.get_test('longclaw_checkout_token')
|
self.get_test('longclaw_checkout_token')
|
||||||
|
|
||||||
|
|
||||||
|
class CheckoutTest(TestCase):
|
||||||
|
|
||||||
|
def test_checkout_form(self):
|
||||||
|
'''
|
||||||
|
Test we can create the form without a shipping option
|
||||||
|
'''
|
||||||
|
data = {
|
||||||
|
'email': 'test@test.com',
|
||||||
|
'different_billing_address': False
|
||||||
|
}
|
||||||
|
form = CheckoutForm(data=data)
|
||||||
|
self.assertTrue(form.is_valid(), form.errors.as_json())
|
||||||
|
|
||||||
|
def test_invalid_checkout_form(self):
|
||||||
|
'''
|
||||||
|
Test making an invalid form
|
||||||
|
'''
|
||||||
|
form = CheckoutForm({
|
||||||
|
'email': ''
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
def test_get_checkout(self):
|
||||||
|
'''
|
||||||
|
Test the checkout GET view
|
||||||
|
'''
|
||||||
|
request = RequestFactory().get(reverse('longclaw_checkout_view'))
|
||||||
|
response = CheckoutView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_post_checkout(self):
|
||||||
|
'''
|
||||||
|
Test correctly posting to the checkout view
|
||||||
|
'''
|
||||||
|
country = CountryFactory()
|
||||||
|
request = RequestFactory().post(
|
||||||
|
reverse('longclaw_checkout_view'),
|
||||||
|
{
|
||||||
|
'shipping-name': 'bob',
|
||||||
|
'shipping-line_1': 'blah blah',
|
||||||
|
'shipping-postcode': 'ytxx 23x',
|
||||||
|
'shipping-city': 'London',
|
||||||
|
'shipping-country': country.pk,
|
||||||
|
'email': 'test@test.com'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request.session = {}
|
||||||
|
bid = basket_id(request)
|
||||||
|
BasketItemFactory(basket_id=bid)
|
||||||
|
response = CheckoutView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_post_checkout_billing(self):
|
||||||
|
'''
|
||||||
|
Test using an alternate shipping
|
||||||
|
address in the checkout view
|
||||||
|
'''
|
||||||
|
country = CountryFactory()
|
||||||
|
request = RequestFactory().post(
|
||||||
|
reverse('longclaw_checkout_view'),
|
||||||
|
{
|
||||||
|
'shipping-name': 'bob',
|
||||||
|
'shipping-line_1': 'blah blah',
|
||||||
|
'shipping-postcode': 'ytxx 23x',
|
||||||
|
'shipping-city': 'London',
|
||||||
|
'shipping-country': country.pk,
|
||||||
|
'billing-name': 'john',
|
||||||
|
'billing-line_1': 'somewhere',
|
||||||
|
'billing-postcode': 'lmewrewr',
|
||||||
|
'billing-city': 'London',
|
||||||
|
'billing-country': country.pk,
|
||||||
|
'email': 'test@test.com',
|
||||||
|
'different_billing_address': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request.session = {}
|
||||||
|
bid = basket_id(request)
|
||||||
|
BasketItemFactory(basket_id=bid)
|
||||||
|
response = CheckoutView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_post_checkout_invalid(self):
|
||||||
|
'''
|
||||||
|
Test posting an invalid form.
|
||||||
|
This should return a 200 response - rerendering
|
||||||
|
the form page with the errors
|
||||||
|
'''
|
||||||
|
request = RequestFactory().post(
|
||||||
|
reverse('longclaw_checkout_view')
|
||||||
|
)
|
||||||
|
request.session = {}
|
||||||
|
bid = basket_id(request)
|
||||||
|
BasketItemFactory(basket_id=bid)
|
||||||
|
response = CheckoutView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_checkout_success(self):
|
||||||
|
'''
|
||||||
|
Test the checkout success view
|
||||||
|
'''
|
||||||
|
address = AddressFactory()
|
||||||
|
order = OrderFactory(shipping_address=address, billing_address=address)
|
||||||
|
response = self.client.get(reverse('longclaw_checkout_success', kwargs={'pk': order.id}))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from longclaw.longclawcheckout import api
|
from longclaw.longclawcheckout import api, views
|
||||||
from longclaw.settings import API_URL_PREFIX
|
from longclaw.settings import API_URL_PREFIX
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -11,5 +11,11 @@ urlpatterns = [
|
||||||
name='longclaw_checkout_prepaid'),
|
name='longclaw_checkout_prepaid'),
|
||||||
url(API_URL_PREFIX + r'checkout/token/$',
|
url(API_URL_PREFIX + r'checkout/token/$',
|
||||||
api.create_token,
|
api.create_token,
|
||||||
name='longclaw_checkout_token')
|
name='longclaw_checkout_token'),
|
||||||
]
|
url(r'checkout/$',
|
||||||
|
views.CheckoutView.as_view(),
|
||||||
|
name='longclaw_checkout_view'),
|
||||||
|
url(r'checkout/success/(?P<pk>[0-9]+)/$',
|
||||||
|
views.checkout_success,
|
||||||
|
name='longclaw_checkout_success')
|
||||||
|
]
|
||||||
|
|
|
@ -1,41 +1,76 @@
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
from django.utils import timezone
|
||||||
|
from ipware.ip import get_real_ip
|
||||||
|
|
||||||
|
from longclaw.longclawbasket.utils import get_basket_items, destroy_basket
|
||||||
|
from longclaw.longclawshipping.utils import get_shipping_cost
|
||||||
|
|
||||||
from longclaw.longclaworders.models import Order, OrderItem
|
from longclaw.longclaworders.models import Order, OrderItem
|
||||||
from longclaw.longclawshipping.models import Address
|
from longclaw.longclawshipping.models import Address
|
||||||
|
from longclaw.longclawsettings.models import LongclawSettings
|
||||||
|
from longclaw import settings
|
||||||
|
|
||||||
class PaymentError(Exception):
|
GATEWAY = import_string(settings.PAYMENT_GATEWAY)()
|
||||||
def __init__(self, message):
|
|
||||||
self.message = str(message)
|
|
||||||
|
|
||||||
def create_order(basket_items,
|
def create_order(email,
|
||||||
addresses,
|
request,
|
||||||
email,
|
addresses=None,
|
||||||
shipping_rate,
|
shipping_address=None,
|
||||||
ip_address='0.0.0.0'):
|
billing_address=None,
|
||||||
|
shipping_option=None,
|
||||||
|
capture_payment=False):
|
||||||
'''
|
'''
|
||||||
Create an order from a basket and customer infomation
|
Create an order from a basket and customer infomation
|
||||||
'''
|
'''
|
||||||
if isinstance(addresses, dict):
|
basket_items, _ = get_basket_items(request)
|
||||||
shipping_address, _ = Address.objects.get_or_create(name=addresses['shipping_name'],
|
if addresses:
|
||||||
|
# Longclaw < 0.2 used 'shipping_name', longclaw > 0.2 uses a consistent
|
||||||
|
# prefix (shipping_address_xxxx)
|
||||||
|
try:
|
||||||
|
shipping_name = addresses['shipping_name']
|
||||||
|
except KeyError:
|
||||||
|
shipping_name = addresses['shipping_address_name']
|
||||||
|
|
||||||
|
shipping_country = addresses['shipping_address_country']
|
||||||
|
if not shipping_country:
|
||||||
|
shipping_country = None
|
||||||
|
shipping_address, _ = Address.objects.get_or_create(name=shipping_name,
|
||||||
line_1=addresses[
|
line_1=addresses[
|
||||||
'shipping_address_line1'],
|
'shipping_address_line1'],
|
||||||
city=addresses[
|
city=addresses[
|
||||||
'shipping_address_city'],
|
'shipping_address_city'],
|
||||||
postcode=addresses[
|
postcode=addresses[
|
||||||
'shipping_address_zip'],
|
'shipping_address_zip'],
|
||||||
country=addresses[
|
country=shipping_country)
|
||||||
'shipping_address_country'])
|
|
||||||
shipping_address.save()
|
shipping_address.save()
|
||||||
billing_address, _ = Address.objects.get_or_create(name=addresses['billing_name'],
|
try:
|
||||||
|
billing_name = addresses['billing_name']
|
||||||
|
except KeyError:
|
||||||
|
billing_name = addresses['billing_address_name']
|
||||||
|
billing_country = addresses['shipping_address_country']
|
||||||
|
if not billing_country:
|
||||||
|
billing_country = None
|
||||||
|
billing_address, _ = Address.objects.get_or_create(name=billing_name,
|
||||||
line_1=addresses[
|
line_1=addresses[
|
||||||
'billing_address_line1'],
|
'billing_address_line1'],
|
||||||
city=addresses[
|
city=addresses[
|
||||||
'billing_address_city'],
|
'billing_address_city'],
|
||||||
postcode=addresses[
|
postcode=addresses[
|
||||||
'billing_address_zip'],
|
'billing_address_zip'],
|
||||||
country=addresses[
|
country=billing_country)
|
||||||
'billing_address_country'])
|
|
||||||
billing_address.save()
|
billing_address.save()
|
||||||
|
else:
|
||||||
|
shipping_country = shipping_address.country
|
||||||
|
|
||||||
|
ip_address = get_real_ip(request)
|
||||||
|
if shipping_country and shipping_option:
|
||||||
|
site_settings = LongclawSettings.for_site(request.site)
|
||||||
|
shipping_rate = get_shipping_cost(
|
||||||
|
shipping_address.country.pk,
|
||||||
|
shipping_option,
|
||||||
|
site_settings)
|
||||||
|
else:
|
||||||
|
shipping_rate = 0
|
||||||
order = Order(
|
order = Order(
|
||||||
email=email,
|
email=email,
|
||||||
ip_address=ip_address,
|
ip_address=ip_address,
|
||||||
|
@ -44,8 +79,11 @@ def create_order(basket_items,
|
||||||
shipping_rate=shipping_rate
|
shipping_rate=shipping_rate
|
||||||
)
|
)
|
||||||
order.save()
|
order.save()
|
||||||
# Create the order items
|
|
||||||
|
# Create the order items & compute total
|
||||||
|
total = 0
|
||||||
for item in basket_items:
|
for item in basket_items:
|
||||||
|
total += item.total()
|
||||||
order_item = OrderItem(
|
order_item = OrderItem(
|
||||||
product=item.variant,
|
product=item.variant,
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
|
@ -53,4 +91,13 @@ def create_order(basket_items,
|
||||||
)
|
)
|
||||||
order_item.save()
|
order_item.save()
|
||||||
|
|
||||||
|
if capture_payment:
|
||||||
|
desc = 'Payment from {} for order id #{}'.format(email, order.id)
|
||||||
|
transaction_id = GATEWAY.create_payment(request,
|
||||||
|
float(total) + shipping_rate,
|
||||||
|
description=desc)
|
||||||
|
order.payment_date = timezone.now()
|
||||||
|
order.transaction_id = transaction_id
|
||||||
|
# Once the order has been successfully taken, we can empty the basket
|
||||||
|
destroy_basket(request)
|
||||||
return order
|
return order
|
||||||
|
|
|
@ -1,3 +1,80 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
# Create your views here.
|
try:
|
||||||
|
from django.urls import reverse
|
||||||
|
except ImportError:
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from longclaw.longclawshipping.forms import AddressForm
|
||||||
|
from longclaw.longclawcheckout.forms import CheckoutForm
|
||||||
|
from longclaw.longclawcheckout.utils import create_order
|
||||||
|
from longclaw.longclawbasket.utils import get_basket_items
|
||||||
|
from longclaw.longclaworders.models import Order
|
||||||
|
|
||||||
|
|
||||||
|
@require_GET
|
||||||
|
def checkout_success(request, pk):
|
||||||
|
order = get_object_or_404(Order, id=pk)
|
||||||
|
return render(request, "longclawcheckout/success.html", {'order': order})
|
||||||
|
|
||||||
|
|
||||||
|
class CheckoutView(TemplateView):
|
||||||
|
template_name = "longclawcheckout/checkout.html"
|
||||||
|
checkout_form = CheckoutForm
|
||||||
|
shipping_address_form = AddressForm
|
||||||
|
billing_address_form = AddressForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CheckoutView, self).get_context_data(**kwargs)
|
||||||
|
items, _ = get_basket_items(self.request)
|
||||||
|
total_price = sum(item.total() for item in items)
|
||||||
|
site = getattr(self.request, 'site', None)
|
||||||
|
context['checkout_form'] = self.checkout_form(
|
||||||
|
self.request.POST or None)
|
||||||
|
context['shipping_form'] = self.shipping_address_form(
|
||||||
|
self.request.POST or None,
|
||||||
|
prefix='shipping',
|
||||||
|
site=site)
|
||||||
|
context['billing_form'] = self.billing_address_form(
|
||||||
|
self.request.POST or None,
|
||||||
|
prefix='billing',
|
||||||
|
site=site)
|
||||||
|
context['basket'] = items
|
||||||
|
context['total_price'] = total_price
|
||||||
|
return context
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
context = self.get_context_data(**kwargs)
|
||||||
|
checkout_form = context['checkout_form']
|
||||||
|
shipping_form = context['shipping_form']
|
||||||
|
all_ok = checkout_form.is_valid() and shipping_form.is_valid()
|
||||||
|
if all_ok:
|
||||||
|
email = checkout_form.cleaned_data['email']
|
||||||
|
shipping_option = checkout_form.cleaned_data.get(
|
||||||
|
'shipping_option', None)
|
||||||
|
shipping_address = shipping_form.save()
|
||||||
|
|
||||||
|
if checkout_form.cleaned_data['different_billing_address']:
|
||||||
|
billing_form = context['billing_form']
|
||||||
|
all_ok = billing_form.is_valid()
|
||||||
|
if all_ok:
|
||||||
|
billing_address = billing_form.save()
|
||||||
|
else:
|
||||||
|
billing_address = shipping_address
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
order = create_order(
|
||||||
|
email,
|
||||||
|
request,
|
||||||
|
shipping_address=shipping_address,
|
||||||
|
billing_address=billing_address,
|
||||||
|
shipping_option=shipping_option,
|
||||||
|
capture_payment=True
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(reverse(
|
||||||
|
'longclaw_checkout_success',
|
||||||
|
kwargs={'pk': order.id}))
|
||||||
|
return super(CheckoutView, self).render_to_response(context)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-16 16:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longclaworders', '0007_auto_20170313_0846'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='order',
|
||||||
|
name='shipping_address',
|
||||||
|
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders_shipping_address', to='longclawshipping.Address'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -24,10 +24,12 @@ class Order(models.Model):
|
||||||
ip_address = models.GenericIPAddressField(blank=True, null=True)
|
ip_address = models.GenericIPAddressField(blank=True, null=True)
|
||||||
|
|
||||||
# shipping info
|
# shipping info
|
||||||
shipping_address = models.ForeignKey(Address, related_name="orders_shipping_address")
|
shipping_address = models.ForeignKey(
|
||||||
|
Address, blank=True, related_name="orders_shipping_address")
|
||||||
|
|
||||||
# billing info
|
# billing info
|
||||||
billing_address = models.ForeignKey(Address, blank=True, related_name="orders_billing_address")
|
billing_address = models.ForeignKey(
|
||||||
|
Address, blank=True, related_name="orders_billing_address")
|
||||||
|
|
||||||
shipping_rate = models.DecimalField(max_digits=12,
|
shipping_rate = models.DecimalField(max_digits=12,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
|
|
|
@ -46,6 +46,5 @@ def shipping_cost(request):
|
||||||
def shipping_countries(request):
|
def shipping_countries(request):
|
||||||
''' Get all shipping countries
|
''' Get all shipping countries
|
||||||
'''
|
'''
|
||||||
queryset = models.ShippingRate.objects.all()
|
queryset = models.Country.exclude(shippingrate=None)
|
||||||
country_data = [(c.name, c.code) for obj in queryset for c in obj.countries]
|
return Response(data=queryset, status=status.HTTP_200_OK)
|
||||||
return Response(data=country_data, status=status.HTTP_200_OK)
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
from longclaw.longclawsettings.models import LongclawSettings
|
|
||||||
from longclaw.longclawshipping.models import ShippingRate
|
|
||||||
from django_countries import countries, fields
|
|
||||||
|
|
||||||
class CountryChoices(object):
|
|
||||||
'''
|
|
||||||
Helper class which returns a list of available countries based on
|
|
||||||
the selected shipping options.
|
|
||||||
|
|
||||||
If default_shipping_enabled is ``True`` in the longclaw settings, then
|
|
||||||
all possible countries are returned. Otherwise only countries for
|
|
||||||
which a ``ShippingRate`` has been declared are returned.
|
|
||||||
'''
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
request = kwargs.get('request', None)
|
|
||||||
self._all_countries = True
|
|
||||||
if request:
|
|
||||||
settings = LongclawSettings.for_site(request.site)
|
|
||||||
self._all_countries = settings.default_shipping_enabled
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
if self._all_countries:
|
|
||||||
return countries
|
|
||||||
else:
|
|
||||||
return ShippingRate.objects.values_list('countries').distinct()
|
|
||||||
|
|
||||||
|
|
||||||
class ShippingCountryField(fields.CountryField):
|
|
||||||
'''
|
|
||||||
Country choice field whose choices are constrained by the
|
|
||||||
configured shipping options.
|
|
||||||
'''
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs.update({
|
|
||||||
'countries': CountryChoices(**kwargs)
|
|
||||||
})
|
|
||||||
super(ShippingCountryField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,25 @@
|
||||||
|
from django.forms import ModelForm, ModelChoiceField
|
||||||
|
from longclaw.longclawsettings.models import LongclawSettings
|
||||||
|
from longclaw.longclawshipping.models import Address, Country
|
||||||
|
|
||||||
|
class AddressForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Address
|
||||||
|
fields = ['name', 'line_1', 'line_2', 'city', 'postcode', 'country']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
site = kwargs.pop('site', None)
|
||||||
|
super(AddressForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Edit the country field to only contain
|
||||||
|
# countries specified for shipping
|
||||||
|
all_countries = True
|
||||||
|
if site:
|
||||||
|
settings = LongclawSettings.for_site(site)
|
||||||
|
all_countries = settings.default_shipping_enabled
|
||||||
|
if all_countries:
|
||||||
|
queryset = Country.objects.all()
|
||||||
|
else:
|
||||||
|
queryset = Country.objects.exclude(shippingrate=None)
|
||||||
|
self.fields['country'] = ModelChoiceField(queryset)
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-16 16:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longclawshipping', '0002_auto_20170410_1620'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Country',
|
||||||
|
fields=[
|
||||||
|
('iso', models.CharField(max_length=2, primary_key=True, serialize=False)),
|
||||||
|
('name_official', models.CharField(max_length=128)),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('sort_priority', models.PositiveIntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Countries',
|
||||||
|
'ordering': ('-sort_priority', 'name'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shippingrate',
|
||||||
|
name='countries',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shippingrate',
|
||||||
|
name='countries',
|
||||||
|
field=models.ManyToManyField(to='longclawshipping.Country'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-18 10:26
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longclawshipping', '0003_auto_20170516_1629'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='country',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='longclawshipping.Country'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-18 10:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longclawshipping', '0004_auto_20170518_0526'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='country',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='longclawshipping.Country'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-05-21 08:31
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longclawshipping', '0005_auto_20170518_0558'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='shippingrate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Unique name to refer to this shipping rate by', max_length=32, unique=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,7 +3,6 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
||||||
from wagtail.wagtailsnippets.models import register_snippet
|
from wagtail.wagtailsnippets.models import register_snippet
|
||||||
from django_countries.fields import CountryField
|
|
||||||
|
|
||||||
@register_snippet
|
@register_snippet
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
|
@ -13,7 +12,7 @@ class Address(models.Model):
|
||||||
line_2 = models.CharField(max_length=128, blank=True)
|
line_2 = models.CharField(max_length=128, blank=True)
|
||||||
city = models.CharField(max_length=64)
|
city = models.CharField(max_length=64)
|
||||||
postcode = models.CharField(max_length=10)
|
postcode = models.CharField(max_length=10)
|
||||||
country = CountryField()
|
country = models.ForeignKey('longclawshipping.Country', blank=True, null=True)
|
||||||
|
|
||||||
panels = [
|
panels = [
|
||||||
FieldPanel('name'),
|
FieldPanel('name'),
|
||||||
|
@ -33,11 +32,15 @@ class ShippingRate(models.Model):
|
||||||
An individual shipping rate. This can be applied to
|
An individual shipping rate. This can be applied to
|
||||||
multiple countries.
|
multiple countries.
|
||||||
'''
|
'''
|
||||||
name = models.CharField(max_length=32, unique=True)
|
name = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
unique=True,
|
||||||
|
help_text="Unique name to refer to this shipping rate by"
|
||||||
|
)
|
||||||
rate = models.DecimalField(max_digits=12, decimal_places=2)
|
rate = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
carrier = models.CharField(max_length=64)
|
carrier = models.CharField(max_length=64)
|
||||||
description = models.CharField(max_length=128)
|
description = models.CharField(max_length=128)
|
||||||
countries = CountryField(multiple=True)
|
countries = models.ManyToManyField('longclawshipping.Country')
|
||||||
|
|
||||||
panels = [
|
panels = [
|
||||||
FieldPanel('name'),
|
FieldPanel('name'),
|
||||||
|
@ -49,3 +52,33 @@ class ShippingRate(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Country(models.Model):
|
||||||
|
"""
|
||||||
|
International Organization for Standardization (ISO) 3166-1 Country list
|
||||||
|
Instance Variables:
|
||||||
|
iso -- ISO 3166-1 alpha-2
|
||||||
|
name -- Official country names (in all caps) used by the ISO 3166
|
||||||
|
display_name -- Country names in title format
|
||||||
|
sort_priority -- field that allows for customizing the default ordering
|
||||||
|
0 is the default value, and the higher the value the closer to the
|
||||||
|
beginning of the list it will be. An example use case would be you will
|
||||||
|
primarily have addresses for one country, so you want that particular
|
||||||
|
country to be the first option in an html dropdown box. To do this, you
|
||||||
|
would simply change the value in the json file or alter
|
||||||
|
country_grabber.py's priority dictionary and run it to regenerate
|
||||||
|
the json
|
||||||
|
"""
|
||||||
|
iso = models.CharField(max_length=2, primary_key=True)
|
||||||
|
name_official = models.CharField(max_length=128)
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
sort_priority = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = 'Countries'
|
||||||
|
ordering = ('-sort_priority', 'name',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
''' Return the display form of the country name'''
|
||||||
|
return self.name
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django_countries.serializer_fields import CountryField
|
|
||||||
|
|
||||||
from longclaw.longclawshipping.models import Address, ShippingRate
|
from longclaw.longclawshipping.models import Address, ShippingRate, Country
|
||||||
|
|
||||||
class AddressSerializer(serializers.ModelSerializer):
|
class AddressSerializer(serializers.ModelSerializer):
|
||||||
country = CountryField()
|
country = serializers.PrimaryKeyRelatedField(queryset=Country.objects.all())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address
|
model = Address
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
class ShippingRateSerializer(serializers.ModelSerializer):
|
class ShippingRateSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShippingRate
|
model = ShippingRate
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
class CountrySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = "__all__"
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
from longclaw.tests.utils import LongclawTestCase
|
from django.test import TestCase
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
from longclaw.tests.utils import LongclawTestCase, AddressFactory, CountryFactory
|
||||||
|
from longclaw.longclawshipping.forms import AddressForm
|
||||||
|
|
||||||
class AddressTest(LongclawTestCase):
|
class AddressTest(LongclawTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.country = CountryFactory()
|
||||||
def test_create_address(self):
|
def test_create_address(self):
|
||||||
"""
|
"""
|
||||||
Test creating an address object via the api
|
Test creating an address object via the api
|
||||||
|
@ -11,6 +15,16 @@ class AddressTest(LongclawTestCase):
|
||||||
'line_1': 'Bobstreet',
|
'line_1': 'Bobstreet',
|
||||||
'city': 'Bobsville',
|
'city': 'Bobsville',
|
||||||
'postcode': 'BOB22 2BO',
|
'postcode': 'BOB22 2BO',
|
||||||
'country': 'UK'
|
'country': self.country.pk
|
||||||
}
|
}
|
||||||
self.post_test(data, 'longclaw_address_list')
|
self.post_test(data, 'longclaw_address_list')
|
||||||
|
|
||||||
|
|
||||||
|
class AddressFormTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.address = AddressFactory()
|
||||||
|
|
||||||
|
def test_address_form(self):
|
||||||
|
form = AddressForm(data=model_to_dict(self.address))
|
||||||
|
self.assertTrue(form.is_valid(), form.errors.as_json())
|
||||||
|
|
|
@ -7,12 +7,15 @@ class InvalidShippingRate(Exception):
|
||||||
class InvalidShippingCountry(Exception):
|
class InvalidShippingCountry(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_shipping_cost(country_code, option, settings):
|
def get_shipping_cost(country_code, name, settings):
|
||||||
|
"""
|
||||||
|
Return the shipping cost for a given country code and shipping option (shipping rate name)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
qrs = models.ShippingRate.objects.filter(countries__contains=country_code)
|
qrs = models.ShippingRate.objects.filter(countries__in=[country_code])
|
||||||
try:
|
try:
|
||||||
if qrs.count() > 1:
|
if qrs.count() > 1:
|
||||||
shipping_rate = qrs.filter(name=option)[0]
|
shipping_rate = qrs.filter(name=name)[0]
|
||||||
else:
|
else:
|
||||||
shipping_rate = qrs[0]
|
shipping_rate = qrs[0]
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% templatetag openblock %} extends "base.html" {% templatetag closeblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% templatetag openblock %} extends "base.html" {% templatetag closeblock %}
|
|
@ -23,6 +23,7 @@ STRIPE_PUBLISHABLE = getattr(settings, 'STRIPE_PUBLISHABLE', '')
|
||||||
STRIPE_SECRET = getattr(settings, 'STRIPE_SECRET', '')
|
STRIPE_SECRET = getattr(settings, 'STRIPE_SECRET', '')
|
||||||
|
|
||||||
# Only required if using Braintree as the payment gateway
|
# Only required if using Braintree as the payment gateway
|
||||||
|
BRAINTREE_SANDBOX = getattr(settings, 'BRAINTREE_SANDBOX', False)
|
||||||
BRAINTREE_MERCHANT_ID = getattr(settings, 'BRAINTREE_MERCHANT_ID', '')
|
BRAINTREE_MERCHANT_ID = getattr(settings, 'BRAINTREE_MERCHANT_ID', '')
|
||||||
BRAINTREE_PUBLIC_KEY = getattr(settings, 'BRAINTREE_PUBLIC_KEY', '')
|
BRAINTREE_PUBLIC_KEY = getattr(settings, 'BRAINTREE_PUBLIC_KEY', '')
|
||||||
BRAINTREE_PRIVATE_KEY = getattr(settings, 'BRAINTREE_PRIVATE_KEY', '')
|
BRAINTREE_PRIVATE_KEY = getattr(settings, 'BRAINTREE_PRIVATE_KEY', '')
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
# -*- coding: utf-8
|
# -*- coding: utf-8
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
import os
|
||||||
import django
|
import django
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"
|
SECRET_KEY = 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk'
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
'default': {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
"NAME": ":memory:",
|
'NAME': ':memory:',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = "tests.urls"
|
ROOT_URLCONF = 'longclaw.tests.urls'
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
'django.contrib.auth',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.sites",
|
'django.contrib.sites',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
|
|
||||||
'wagtail.wagtailforms',
|
'wagtail.wagtailforms',
|
||||||
|
@ -42,24 +42,39 @@ INSTALLED_APPS = [
|
||||||
'taggit',
|
'taggit',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django_countries',
|
|
||||||
|
|
||||||
"longclaw.longclawsettings",
|
'longclaw.longclawsettings',
|
||||||
"longclaw.longclawshipping",
|
'longclaw.longclawshipping',
|
||||||
"longclaw.longclawproducts",
|
'longclaw.longclawproducts',
|
||||||
"longclaw.longclaworders",
|
'longclaw.longclaworders',
|
||||||
"longclaw.longclawcheckout",
|
'longclaw.longclawcheckout',
|
||||||
"longclaw.longclawbasket",
|
'longclaw.longclawbasket',
|
||||||
"longclaw.tests.products"
|
'longclaw.tests.products',
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
if django.VERSION >= (1, 10):
|
MIDDLEWARE = [
|
||||||
MIDDLEWARE = ()
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
else:
|
'django.middleware.common.CommonMiddleware',
|
||||||
MIDDLEWARE_CLASSES = ()
|
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||||
|
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [
|
||||||
|
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||||
|
],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if django.VERSION >= (1, 10):
|
||||||
|
MIDDLEWARE = MIDDLEWARE
|
||||||
|
else:
|
||||||
|
MIDDLEWARE_CLASSES = MIDDLEWARE
|
||||||
|
|
||||||
PRODUCT_VARIANT_MODEL = 'products.ProductVariant'
|
PRODUCT_VARIANT_MODEL = 'products.ProductVariant'
|
||||||
|
|
|
@ -8,8 +8,55 @@ from wagtail_factories import PageFactory
|
||||||
|
|
||||||
from longclaw.longclawproducts.models import Product
|
from longclaw.longclawproducts.models import Product
|
||||||
from longclaw.longclawbasket.models import BasketItem
|
from longclaw.longclawbasket.models import BasketItem
|
||||||
|
from longclaw.longclaworders.models import Order, OrderItem
|
||||||
|
from longclaw.longclawshipping.models import Address, Country, ShippingRate
|
||||||
from longclaw.utils import ProductVariant
|
from longclaw.utils import ProductVariant
|
||||||
|
|
||||||
|
class OrderFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
|
||||||
|
class CountryFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
|
||||||
|
iso = factory.Faker('pystr', max_chars=2, min_chars=2)
|
||||||
|
name_official = factory.Faker('text', max_nb_chars=128)
|
||||||
|
name = factory.Faker('text', max_nb_chars=128)
|
||||||
|
|
||||||
|
|
||||||
|
class AddressFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Address
|
||||||
|
|
||||||
|
name = factory.Faker('text', max_nb_chars=64)
|
||||||
|
line_1 = factory.Faker('text', max_nb_chars=128)
|
||||||
|
line_2 = factory.Faker('text', max_nb_chars=128)
|
||||||
|
city = factory.Faker('text', max_nb_chars=64)
|
||||||
|
postcode = factory.Faker('text', max_nb_chars=10)
|
||||||
|
country = factory.SubFactory(CountryFactory)
|
||||||
|
|
||||||
|
class ShippingRateFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = ShippingRate
|
||||||
|
|
||||||
|
name = factory.Faker('text', max_nb_chars=32)
|
||||||
|
rate = 1.0
|
||||||
|
carrier = 'Royal Mail'
|
||||||
|
description = 'Test'
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def countries(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
# Simple build, do nothing.
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
# A list of countries were passed in, use them
|
||||||
|
for country in extracted:
|
||||||
|
self.countries.add(country)
|
||||||
|
|
||||||
class ProductFactory(PageFactory):
|
class ProductFactory(PageFactory):
|
||||||
''' Create a random Product
|
''' Create a random Product
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
django-countries>=4.1
|
django-countries>=4.1
|
||||||
django-extensions==1.7.5
|
django-extensions==1.7.5
|
||||||
|
django-ipware>=1.1.6
|
||||||
djangorestframework==3.5.4
|
djangorestframework==3.5.4
|
||||||
wagtail>=1.7
|
wagtail>=1.7
|
||||||
|
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -90,7 +90,8 @@ setup(
|
||||||
'wagtail>=1.7',
|
'wagtail>=1.7',
|
||||||
'django-countries>=4.3',
|
'django-countries>=4.3',
|
||||||
'django-extensions>=1.7.5',
|
'django-extensions>=1.7.5',
|
||||||
'djangorestframework>=3.5.4'
|
'djangorestframework>=3.5.4',
|
||||||
|
'django-ipware>=1.1.6'
|
||||||
],
|
],
|
||||||
license="MIT",
|
license="MIT",
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|
Ładowanie…
Reference in New Issue