Skip to content

Commit f4f1a4d

Browse files
committed
Modularity and documentation enhancements
1 parent 90ffad4 commit f4f1a4d

18 files changed

+186
-265
lines changed

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,35 @@ Add payment and import_export to your `INSTALLED_APPS`:
2525
)
2626

2727

28-
Run the migrations:
28+
Create the payment tables by running the migrations:
2929

3030
./manage.py migrate
3131

3232

33+
Add payment urls to your urlpatterns:
34+
35+
urlpatterns = [
36+
...
37+
path('payment/', include('payment.urls')),
38+
...
39+
]
40+
41+
3342
Configure the CHECKOUT_PAYMENT_GATEWAYS and PAYMENT_GATEWAYS settings. See [example settings.py](example_project/settings.py)
3443

3544

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

3948
### Stripe
40-
Implemented features:
41-
- Simple Payment (authorization and capture at once)
42-
- Separate authorization and capture
43-
- Refunds
49+
- Authorization
50+
- Capture
51+
- Refund
4452
- Split Payment with stripe connect
4553
- Adding metadata to the stripe payment, for easy sorting in stripe
4654

55+
[More Stripe information](docs/stripe.md)
56+
4757

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

6878
http://127.0.0.1:8000/admin
6979

70-
Create a new payment.
80+
Create a new payment (make sure the captured amount currency is the same as the total currency)
7181

7282
Then operate on that payment with:
7383

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

96106

97-
## More information
98-
99-
* [The design of this application](docs/design.md)
107+
More information about [the design of this application](docs/design.md)

docs/design.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ This package contains:
4848
- A modified copy of stripe/init.py (to fix a bug when we just want to authorize)
4949
- A new localized django admin for payments and transactions.
5050

51+
52+
Possible enhancements
53+
---------------------
54+
55+
Right now the beginning of the flow for all gateways is implemented outside of the Payment abstractions.
56+
Maybe implement the authorization phase inside the payment abstraction?

docs/payment-with-stripe.png

-217 KB
Binary file not shown.

docs/stripe-authorization.png

169 KB
Loading

docs/stripe.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Stripe
2+
3+
## General flow
4+
5+
![Payment authorization with stripe](stripe-authorization.png)
6+
7+
8+
## Split payments
9+
10+
There three different ways to use stripe connect: https://stripe.com/docs/connect/charges#choosing-approach
11+
12+
We choose to use destination charges: https://stripe.com/docs/connect/destination-charges
13+
14+
For destination charges there are again two choices on how to split the funds: `application_fee` or `amount`.
15+
16+
We've implemented the `amount` option.
17+
18+
19+
Split payments are configured with an optional `STRIPE_CONNECT` setting:
20+
21+
transfer_destination is the id of the destination account ().
22+
transfer_percent is a number between 0 and 100. For instance 90 means that the destination will receive 90% of the charge amount.
23+
24+
Example:
25+
26+
```
27+
STRIPE_CONNECT = {
28+
'transfer_destination': 'acct_1Es1XuECDoeqctKE',
29+
'transfer_percent': 90
30+
}
31+
```

example_project/settings.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -77,45 +77,43 @@ def abspath(*args):
7777
},
7878
]
7979

80+
DATETIME_FORMAT = 'Y-m-d @ H:i:s e'
81+
8082
# Enable specific currencies (djmoney)
8183
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF']
8284

83-
DUMMY = "dummy"
84-
STRIPE = "stripe"
85+
DUMMY = 'dummy'
86+
STRIPE = 'stripe'
8587

8688
CHECKOUT_PAYMENT_GATEWAYS = {
87-
DUMMY: "Dummy gateway",
88-
STRIPE: "Stripe",
89+
DUMMY: 'Dummy gateway',
90+
STRIPE: 'Stripe',
8991
}
9092

9193
PAYMENT_GATEWAYS = {
9294
DUMMY: {
93-
"module": "payment.gateways.dummy",
94-
"config": {
95-
"auto_capture": True,
96-
"connection_params": {},
97-
"template_path": "payment/dummy.html",
95+
'module': 'payment.gateways.dummy',
96+
'config': {
97+
'auto_capture': True,
98+
'connection_params': {},
99+
'template_path': 'payment/dummy.html',
98100
},
99101
},
100102
STRIPE: {
101-
"module": "payment.gateways.stripe",
102-
"config": {
103-
"auto_capture": True,
104-
"template_path": "payment/stripe.html",
105-
"connection_params": {
106-
"public_key": os.environ.get("STRIPE_PUBLIC_KEY"),
107-
"secret_key": os.environ.get("STRIPE_SECRET_KEY"),
108-
"store_name": os.environ.get("STRIPE_STORE_NAME", "skioo shop"),
109-
"store_image": os.environ.get("STRIPE_STORE_IMAGE", None),
110-
"prefill": os.environ.get("STRIPE_PREFILL", True),
111-
"remember_me": os.environ.get("STRIPE_REMEMBER_ME", False),
112-
"locale": os.environ.get("STRIPE_LOCALE", "auto"),
113-
"enable_billing_address": os.environ.get(
114-
"STRIPE_ENABLE_BILLING_ADDRESS", False
115-
),
116-
"enable_shipping_address": os.environ.get(
117-
"STRIPE_ENABLE_SHIPPING_ADDRESS", False
118-
),
103+
'module': 'payment.gateways.stripe',
104+
'config': {
105+
'auto_capture': True,
106+
'template_path': 'payment/stripe.html',
107+
'connection_params': {
108+
'public_key': os.environ.get('STRIPE_PUBLIC_KEY'),
109+
'secret_key': os.environ.get('STRIPE_SECRET_KEY'),
110+
'store_name': os.environ.get('STRIPE_STORE_NAME', 'skioo shop'),
111+
'store_image': os.environ.get('STRIPE_STORE_IMAGE', None),
112+
'prefill': os.environ.get('STRIPE_PREFILL', True),
113+
'remember_me': os.environ.get('STRIPE_REMEMBER_ME', False),
114+
'locale': os.environ.get('STRIPE_LOCALE', 'auto'),
115+
'enable_billing_address': os.environ.get('STRIPE_ENABLE_BILLING_ADDRESS', False),
116+
'enable_shipping_address': os.environ.get('STRIPE_ENABLE_SHIPPING_ADDRESS', False),
119117
},
120118
},
121119
},
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
<html>
2+
3+
<body>
4+
15
<H3>Payment</H3>
26
<ul>
37
<li>gateway: {{ payment.gateway }}</li>
@@ -8,13 +12,20 @@ <H3>Payment</H3>
812
<li>captured amount: {{ payment.captured_amount }}</li>
913
</ul>
1014

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

13-
<H3>Stripe Operations:</H3>
17+
<H3>Operations</H3>
1418
<ul>
19+
{% if payment.gateway == 'stripe' %}
1520
<li><a href="{% url 'stripe_elements_token' payment.id %}">Authorize - Elements token</a></li>
1621
<li><a href="{% url 'stripe_checkout' payment.id %}">Authorize - Checkout</a></li>
17-
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a></li>
18-
<li><a href="{% url 'stripe_capture' payment.id %}">Capture</a></li>
22+
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a>
23+
{% endif %}
24+
25+
<li><a href="{% url 'capture' payment.id %}">Capture</a></li>
1926
</ul>
2027

28+
</body>
29+
30+
</html>
31+

example_project/templates/stripe/elements_token.html

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,6 @@
132132
form.submit();
133133
}
134134

135-
136-
137-
138-
139-
140135
</script>
141136

142137
</body>

example_project/urls.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
1-
from django.conf import settings
2-
from django.conf.urls.static import static
31
from django.contrib import admin
42
from django.urls import path, include
53

4+
import views
65
from views import stripe
7-
from views import view_payment
86

9-
urlpatterns = [
10-
path('admin/', admin.site.urls),
11-
path('payment/<payment_id>', view_payment, name='view_payment'),
7+
example_urlpatterns = [
8+
path('<payment_id>', views.view_payment, name='view_payment'),
9+
path('<payment_id>/capture', views.capture, name='capture'),
10+
path('stripe/', include(stripe.urls)),
1211
]
1312

14-
stripe_urls = [
15-
path('<payment_id>/stripe/checkout', stripe.checkout, name='stripe_checkout'),
16-
path('<payment_id>/stripe/elements_token', stripe.elements_token, name='stripe_elements_token'),
17-
18-
path('<payment_id>/stripe/payment_intents_manual_flow', stripe.payment_intents_manual_flow,
19-
name='stripe_payment_intents_manual_flow'),
20-
path('<payment_id>/stripe/payment_intents_confirm_payment', stripe.payment_intents_confirm_payment,
21-
name='stripe_payment_intents_confirm_payment'),
22-
path('<payment_id>/stripe/capture', stripe.capture, name='stripe_capture'),
13+
urlpatterns = [
14+
path('admin/', admin.site.urls),
15+
path('payment/', include('payment.urls')),
16+
path('example/', include(example_urlpatterns)),
2317
]
24-
25-
26-
urlpatterns += [path('stripe', include(stripe_urls))]
27-
28-
29-
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

example_project/views/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
from django.http import HttpRequest, HttpResponse
2-
from django.shortcuts import get_object_or_404
2+
from django.shortcuts import get_object_or_404, redirect
33
from django.template.response import TemplateResponse
4+
from structlog import get_logger
45

56
from payment.models import Payment
7+
from payment.utils import gateway_capture
8+
9+
logger = get_logger()
610

711

812
def view_payment(request: HttpRequest, payment_id: int) -> HttpResponse:
913
payment = get_object_or_404(Payment, id=payment_id)
1014
return TemplateResponse(request, 'operation_list.html', {'payment': payment})
15+
16+
17+
def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
18+
payment = get_object_or_404(Payment, id=payment_id)
19+
capture_result = gateway_capture(payment=payment)
20+
logger.info('capture', payment=payment, capture_result=capture_result)
21+
return redirect('view_payment', payment_id=payment_id)

example_project/views/stripe.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
11
from django.http import HttpRequest, HttpResponse, JsonResponse
22
from django.shortcuts import get_object_or_404, redirect
33
from django.template.response import TemplateResponse
4-
from django.urls import reverse
4+
from django.urls import reverse, path
55
from django.views.decorators.csrf import csrf_exempt
66
from structlog import get_logger
77

88
from payment import get_payment_gateway
99
from payment.gateways.stripe import get_amount_for_stripe, get_currency_for_stripe
1010
from payment.models import Payment
11-
from payment.utils import gateway_authorize, gateway_capture, gateway_refund
11+
from payment.utils import gateway_authorize
1212

1313
logger = get_logger()
1414

1515

16-
def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
16+
@csrf_exempt
17+
def elements_token(request: HttpRequest, payment_id: int) -> HttpResponse:
1718
payment = get_object_or_404(Payment, id=payment_id)
18-
capture_result = gateway_capture(payment=payment)
19-
logger.info('stripe capture', payment=payment, capture_result=capture_result)
20-
return redirect('view_payment', payment_id=payment_id)
19+
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
20+
connection_params = gateway_config.connection_params
21+
22+
if request.method == 'GET':
23+
return TemplateResponse(
24+
request,
25+
'stripe/elements_token.html',
26+
{'stripe_public_key': connection_params['public_key']})
27+
elif request.method == 'POST':
28+
stripe_token = request.POST.get('stripeToken')
29+
if stripe_token is None:
30+
return HttpResponse('missing stripe token')
31+
try:
32+
logger.info('stripe authorize', payment=payment)
33+
gateway_authorize(payment=payment, payment_token=stripe_token)
34+
except Exception as exc:
35+
logger.error('stripe authorize', exc_info=exc)
36+
return HttpResponse('Error authorizing {}: {}'.format(payment_id, exc))
37+
else:
38+
return redirect('view_payment', payment_id=payment.pk)
2139

2240

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

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

5069

51-
@csrf_exempt
52-
def elements_token(request: HttpRequest, payment_id: int) -> HttpResponse:
53-
payment = get_object_or_404(Payment, id=payment_id)
54-
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
55-
connection_params = gateway_config.connection_params
56-
57-
if request.method == 'GET':
58-
return TemplateResponse(
59-
request,
60-
'stripe/elements_token.html',
61-
{'stripe_public_key': connection_params['public_key']})
62-
elif request.method == 'POST':
63-
stripe_token = request.POST.get('stripeToken')
64-
if stripe_token is None:
65-
return HttpResponse('missing stripe token')
66-
try:
67-
logger.info('stripe authorize', payment=payment)
68-
gateway_authorize(payment=payment, payment_token=stripe_token)
69-
except Exception as exc:
70-
logger.error('stripe authorize', exc_info=exc)
71-
return HttpResponse('Error authorizing {}: {}'.format(payment_id, exc))
72-
else:
73-
return redirect('view_payment', payment_id=payment.pk)
74-
75-
7670
def payment_intents_manual_flow(request: HttpRequest, payment_id: int) -> HttpResponse:
7771
payment = get_object_or_404(Payment, id=payment_id)
7872
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
@@ -129,3 +123,13 @@ def payment_intents_confirm_payment(request, payment_id):
129123
else:
130124
# Invalid status
131125
return JsonResponse({'error': 'Invalid PaymentIntent status'}, status=500)
126+
127+
128+
urls = [
129+
path('elements_token/<payment_id>', elements_token, name='stripe_elements_token'),
130+
path('checkout/<payment_id>', checkout, name='stripe_checkout'),
131+
path('payment_intents_manual_flow/<payment_id>', payment_intents_manual_flow,
132+
name='stripe_payment_intents_manual_flow'),
133+
path('payment_intents_confirm_payment/<payment_id>', payment_intents_confirm_payment,
134+
name='stripe_payment_intents_confirm_payment'),
135+
]

0 commit comments

Comments
 (0)