Skip to content

Commit

Permalink
Modularity and documentation enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
nwolff committed Sep 12, 2019
1 parent ead5a72 commit 2423c40
Show file tree
Hide file tree
Showing 18 changed files with 193 additions and 268 deletions.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,35 @@ Add payment and import_export to your `INSTALLED_APPS`:
)


Run the migrations:
Create the payment tables by running the migrations:

./manage.py migrate


Add payment urls to your urlpatterns:

urlpatterns = [
...
path('payment/', include('payment.urls')),
...
]


Configure the CHECKOUT_PAYMENT_GATEWAYS and PAYMENT_GATEWAYS settings. See [example settings.py](example_project/settings.py)


## Payment gateways
This module provides implementations for the following payment-gateways:

### Stripe
Implemented features:
- Simple Payment (authorization and capture at once)
- Separate authorization and capture
- Refunds
- Authorization
- Capture
- Refund
- Split Payment with stripe connect
- Adding metadata to the stripe payment, for easy sorting in stripe

[More Stripe information](docs/stripe.md)


## The example project
The source distribution includes an example project that lets one exercise
Expand All @@ -67,7 +77,7 @@ Then point your browser to:

http://127.0.0.1:8000/admin

Create a new payment.
Create a new payment (make sure the captured amount currency is the same as the total currency)

Then operate on that payment with:

Expand All @@ -94,6 +104,4 @@ To install the version being developed into another django project:
pip install -e <path-to-this-directory>


## More information

* [The design of this application](docs/design.md)
More information about [the design of this application](docs/design.md)
6 changes: 6 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ This package contains:
- A modified copy of stripe/init.py (to fix a bug when we just want to authorize)
- A new localized django admin for payments and transactions.


Possible enhancements
---------------------

Right now the beginning of the flow for all gateways is implemented outside of the Payment abstractions.
Maybe implement the authorization phase inside the payment abstraction?
Binary file removed docs/payment-with-stripe.png
Binary file not shown.
Binary file added docs/stripe-authorization.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions docs/stripe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Stripe

## General flow

![Payment authorization with stripe](stripe-authorization.png)


## Split payments

There three different ways to use stripe connect: https://stripe.com/docs/connect/charges#choosing-approach

We choose to use destination charges: https://stripe.com/docs/connect/destination-charges

For destination charges there are again two choices on how to split the funds: `application_fee` or `amount`.

We've implemented the `amount` option.


Split payments are configured with an optional `STRIPE_CONNECT` setting:

transfer_destination is the id of the destination account ().
transfer_percent is a number between 0 and 100. For instance 90 means that the destination will receive 90% of the charge amount.

Example:

```
STRIPE_CONNECT = {
'transfer_destination': 'acct_1Es1XuECDoeqctKE',
'transfer_percent': 90
}
```
48 changes: 25 additions & 23 deletions example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,41 +77,43 @@ def abspath(*args):
},
]

DATETIME_FORMAT = 'Y-m-d @ H:i:s e'

# Enable specific currencies (djmoney)
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF']

DUMMY = "dummy"
STRIPE = "stripe"
DUMMY = 'dummy'
STRIPE = 'stripe'

CHECKOUT_PAYMENT_GATEWAYS = {
DUMMY: "Dummy gateway",
STRIPE: "Stripe",
DUMMY: 'Dummy gateway',
STRIPE: 'Stripe',
}

PAYMENT_GATEWAYS = {
DUMMY: {
"module": "payment.gateways.dummy",
"config": {
"auto_capture": True,
"connection_params": {},
"template_path": "payment/dummy.html",
'module': 'payment.gateways.dummy',
'config': {
'auto_capture': True,
'connection_params': {},
'template_path': 'payment/dummy.html',
},
},
STRIPE: {
"module": "payment.gateways.stripe",
"config": {
"auto_capture": True,
"template_path": "payment/stripe.html",
"connection_params": {
"public_key": os.environ.get("STRIPE_PUBLIC_KEY"),
"secret_key": os.environ.get("STRIPE_SECRET_KEY"),
"store_name": os.environ.get("STRIPE_STORE_NAME", "skioo shop"),
"store_image": os.environ.get("STRIPE_STORE_IMAGE", None),
"prefill": os.environ.get("STRIPE_PREFILL", True),
"remember_me": os.environ.get("STRIPE_REMEMBER_ME", False),
"locale": os.environ.get("STRIPE_LOCALE", "auto"),
"enable_billing_address": os.environ.get("STRIPE_ENABLE_BILLING_ADDRESS", False),
"enable_shipping_address": os.environ.get("STRIPE_ENABLE_SHIPPING_ADDRESS", False),
'module': 'payment.gateways.stripe',
'config': {
'auto_capture': True,
'template_path': 'payment/stripe.html',
'connection_params': {
'public_key': os.environ.get('STRIPE_PUBLIC_KEY'),
'secret_key': os.environ.get('STRIPE_SECRET_KEY'),
'store_name': os.environ.get('STRIPE_STORE_NAME', 'skioo shop'),
'store_image': os.environ.get('STRIPE_STORE_IMAGE', None),
'prefill': os.environ.get('STRIPE_PREFILL', True),
'remember_me': os.environ.get('STRIPE_REMEMBER_ME', False),
'locale': os.environ.get('STRIPE_LOCALE', 'auto'),
'enable_billing_address': os.environ.get('STRIPE_ENABLE_BILLING_ADDRESS', False),
'enable_shipping_address': os.environ.get('STRIPE_ENABLE_SHIPPING_ADDRESS', False),
},
},
},
Expand Down
21 changes: 16 additions & 5 deletions example_project/templates/operation_list.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<html>

<body>

<H3>Payment</H3>
<ul>
<li>gateway: {{ payment.gateway }}</li>
Expand All @@ -8,14 +12,21 @@ <H3>Payment</H3>
<li>captured amount: {{ payment.captured_amount }}</li>
</ul>

<a href="{% url 'admin:payment_payment_change' payment.id %}">See payment in admin </a>
<a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin </a>

<H3>Stripe Operations:</H3>
<H3>Operations</H3>
<ul>
{% if payment.gateway == 'stripe' %}
<li><a href="{% url 'stripe_elements_token' payment.id %}">Authorize - Elements token</a></li>
<li><a href="{% url 'stripe_checkout' payment.id %}">Authorize - Checkout</a></li>
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a></li>
<li><a href="{% url 'stripe_capture' payment.id %}">Capture</a></li>
<li><a href="{% url 'stripe_refund' payment.id %}">Refund</a></li>
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a>
{% endif %}

<li><a href="{% url 'capture' payment.id %}">Capture</a></li>
<li><a href="{% url 'refund' payment.id %}">Refund</a></li>
</ul>

</body>

</html>

5 changes: 0 additions & 5 deletions example_project/templates/stripe/elements_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,6 @@
form.submit();
}






</script>

</body>
Expand Down
32 changes: 10 additions & 22 deletions example_project/urls.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

import views
from views import stripe
from views import view_payment

urlpatterns = [
path('admin/', admin.site.urls),
path('payment/<payment_id>', view_payment, name='view_payment'),
example_urlpatterns = [
path('<payment_id>', views.view_payment, name='view_payment'),
path('<payment_id>/capture', views.capture, name='capture'),
path('<payment_id>/refund', views.refund, name='refund'),
path('stripe/', include(stripe.urls)),
]

stripe_urls = [
path('<payment_id>/stripe/checkout', stripe.checkout, name='stripe_checkout'),
path('<payment_id>/stripe/elements_token', stripe.elements_token, name='stripe_elements_token'),

path('<payment_id>/stripe/payment_intents_manual_flow', stripe.payment_intents_manual_flow,
name='stripe_payment_intents_manual_flow'),
path('<payment_id>/stripe/payment_intents_confirm_payment', stripe.payment_intents_confirm_payment,
name='stripe_payment_intents_confirm_payment'),
path('<payment_id>/stripe/capture', stripe.capture, name='stripe_capture'),
path('<payment_id>/stripe/refund', stripe.refund, name='stripe_refund'),
urlpatterns = [
path('admin/', admin.site.urls),
path('payment/', include('payment.urls')),
path('example/', include(example_urlpatterns)),
]


urlpatterns += [path('stripe', include(stripe_urls))]


urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
20 changes: 19 additions & 1 deletion example_project/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from structlog import get_logger

from payment.models import Payment
from payment.utils import gateway_capture, gateway_refund

logger = get_logger()


def view_payment(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
return TemplateResponse(request, 'operation_list.html', {'payment': payment})


def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
capture_result = gateway_capture(payment=payment)
logger.info('capture', payment=payment, capture_result=capture_result)
return redirect('view_payment', payment_id=payment_id)


def refund(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
refund_result = gateway_refund(payment=payment)
logger.info('refund', payment=payment, refund_result=refund_result)
return redirect('view_payment', payment_id=payment_id)
73 changes: 35 additions & 38 deletions example_project/views/stripe.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,48 @@
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.urls import reverse, path
from django.views.decorators.csrf import csrf_exempt
from structlog import get_logger

from payment import get_payment_gateway
from payment.gateways.stripe import get_amount_for_stripe, get_currency_for_stripe
from payment.models import Payment
from payment.utils import gateway_authorize, gateway_capture, gateway_refund
from payment.utils import gateway_authorize

logger = get_logger()


def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
@csrf_exempt
def elements_token(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
capture_result = gateway_capture(payment=payment)
logger.info('stripe capture', payment=payment, capture_result=capture_result)
return redirect('view_payment', payment_id=payment_id)

payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
connection_params = gateway_config.connection_params

def refund(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
refund_result = gateway_refund(payment=payment)
logger.info('stripe refund', payment=payment, refund_result=refund_result)
return redirect('view_payment', payment_id=payment_id)
if request.method == 'GET':
return TemplateResponse(
request,
'stripe/elements_token.html',
{'stripe_public_key': connection_params['public_key']})
elif request.method == 'POST':
stripe_token = request.POST.get('stripeToken')
if stripe_token is None:
return HttpResponse('missing stripe token')
try:
logger.info('stripe authorize', payment=payment)
gateway_authorize(payment=payment, payment_token=stripe_token)
except Exception as exc:
logger.error('stripe authorize', exc_info=exc)
return HttpResponse('Error authorizing {}: {}'.format(payment_id, exc))
else:
return redirect('view_payment', payment_id=payment.pk)


def checkout(request: HttpRequest, payment_id: int) -> HttpResponse:
"""
Takes the user to the stripe checkout page.
This is not part of the gateway abstraction, so we implement it directly using the stripe API
XXX: This is incomplete because we don't fulfill the order in the end (we should most likely use webhooks).
We mainly implemented this to see what the checkout page looks like.
"""
payment = get_object_or_404(Payment, id=payment_id)

Expand All @@ -55,31 +67,6 @@ def checkout(request: HttpRequest, payment_id: int) -> HttpResponse:
return TemplateResponse(request, 'stripe/checkout.html', {'CHECKOUT_SESSION_ID': session.id})


@csrf_exempt
def elements_token(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
connection_params = gateway_config.connection_params

if request.method == 'GET':
return TemplateResponse(
request,
'stripe/elements_token.html',
{'stripe_public_key': connection_params['public_key']})
elif request.method == 'POST':
stripe_token = request.POST.get('stripeToken')
if stripe_token is None:
return HttpResponse('missing stripe token')
try:
logger.info('stripe authorize', payment=payment)
gateway_authorize(payment=payment, payment_token=stripe_token)
except Exception as exc:
logger.error('stripe authorize', exc_info=exc)
return HttpResponse('Error authorizing {}: {}'.format(payment_id, exc))
else:
return redirect('view_payment', payment_id=payment.pk)


def payment_intents_manual_flow(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
Expand Down Expand Up @@ -136,3 +123,13 @@ def payment_intents_confirm_payment(request, payment_id):
else:
# Invalid status
return JsonResponse({'error': 'Invalid PaymentIntent status'}, status=500)


urls = [
path('elements_token/<payment_id>', elements_token, name='stripe_elements_token'),
path('checkout/<payment_id>', checkout, name='stripe_checkout'),
path('payment_intents_manual_flow/<payment_id>', payment_intents_manual_flow,
name='stripe_payment_intents_manual_flow'),
path('payment_intents_confirm_payment/<payment_id>', payment_intents_confirm_payment,
name='stripe_payment_intents_confirm_payment'),
]
Loading

0 comments on commit 2423c40

Please sign in to comment.