Replace legacy Stripe Charge API by Payment{setup, intent}
This commit is contained in:
parent
47148454f6
commit
bf83b750de
10 changed files with 250 additions and 50 deletions
|
@ -61,7 +61,6 @@ router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='paym
|
|||
router.register(r'bill', payviews.BillViewSet, basename='bill')
|
||||
router.register(r'order', payviews.OrderViewSet, basename='order')
|
||||
router.register(r'payment', payviews.PaymentViewSet, basename='payment')
|
||||
router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-methods')
|
||||
|
||||
# VMs
|
||||
router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vm')
|
||||
|
|
17
uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py
Normal file
17
uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-04 17:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0016_auto_20200303_1552'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='paymentmethod',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
13
uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py
Normal file
13
uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-05 08:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0017_auto_20200304_1723'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
23
uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py
Normal file
23
uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-05 08:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0018_auto_20200305_0819'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='paymentmethod',
|
||||
name='stripe_setup_intent_id',
|
||||
field=models.CharField(blank=True, max_length=32, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='paymentmethod',
|
||||
name='stripe_card_id',
|
||||
field=models.CharField(blank=True, max_length=32, null=True),
|
||||
),
|
||||
]
|
18
uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py
Normal file
18
uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-05 09:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0019_auto_20200305_0851'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='paymentmethod',
|
||||
old_name='stripe_card_id',
|
||||
new_name='stripe_payment_method_id',
|
||||
),
|
||||
]
|
|
@ -119,27 +119,31 @@ class PaymentMethod(models.Model):
|
|||
primary = models.BooleanField(default=True)
|
||||
|
||||
# Only used for "Stripe" source
|
||||
stripe_card_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def stripe_card_last4(self):
|
||||
if self.source == 'stripe':
|
||||
card_request = uncloud_pay.stripe.get_card(
|
||||
StripeCustomer.objects.get(owner=self.owner).stripe_id,
|
||||
self.stripe_card_id)
|
||||
if card_request['error'] == None:
|
||||
return card_request['response_object']['last4']
|
||||
else:
|
||||
return None
|
||||
if self.source == 'stripe' and self.active:
|
||||
payment_method = uncloud_pay.stripe.get_payment_method(
|
||||
self.stripe_payment_method_id)
|
||||
return payment_method.card.last4
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
if self.source == 'stripe' and self.stripe_payment_method_id != None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def charge(self, amount):
|
||||
if amount > 0: # Make sure we don't charge negative amount by errors...
|
||||
if self.source == 'stripe':
|
||||
stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
|
||||
charge_request = uncloud_pay.stripe.charge_customer(amount, stripe_customer, self.stripe_card_id)
|
||||
charge_request = uncloud_pay.stripe.charge_customer(
|
||||
amount, stripe_customer, self.stripe_payment_method_id)
|
||||
if charge_request['error'] == None:
|
||||
payment = Payment(owner=self.owner, source=self.source, amount=amount)
|
||||
payment.save() # TODO: Check return status
|
||||
|
@ -163,7 +167,8 @@ class PaymentMethod(models.Model):
|
|||
return None
|
||||
|
||||
class Meta:
|
||||
unique_together = [['owner', 'primary']]
|
||||
#API_keyunique_together = [['owner', 'primary']]
|
||||
pass
|
||||
|
||||
###
|
||||
# Bills & Payments.
|
||||
|
|
|
@ -29,7 +29,7 @@ class PaymentMethodSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = PaymentMethod
|
||||
fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4']
|
||||
fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active']
|
||||
|
||||
class ChargePaymentMethodSerializer(serializers.Serializer):
|
||||
amount = serializers.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
@ -41,11 +41,10 @@ class CreditCardSerializer(serializers.Serializer):
|
|||
cvc = serializers.IntegerField()
|
||||
|
||||
class CreatePaymentMethodSerializer(serializers.ModelSerializer):
|
||||
credit_card = CreditCardSerializer()
|
||||
|
||||
please_visit = serializers.CharField(read_only=True)
|
||||
class Meta:
|
||||
model = PaymentMethod
|
||||
fields = ['source', 'description', 'primary', 'credit_card']
|
||||
fields = ['source', 'description', 'primary', 'please_visit']
|
||||
|
||||
###
|
||||
# Orders & Products.
|
||||
|
|
|
@ -10,6 +10,10 @@ import uncloud.secrets
|
|||
# Static stripe configuration used below.
|
||||
CURRENCY = 'chf'
|
||||
|
||||
# README: We use the Payment Intent API as described on
|
||||
# https://stripe.com/docs/payments/save-and-reuse
|
||||
|
||||
# For internal use only.
|
||||
stripe.api_key = uncloud.secrets.STRIPE_KEY
|
||||
|
||||
# Helper (decorator) used to catch errors raised by stripe logic.
|
||||
|
@ -82,6 +86,9 @@ class CreditCard():
|
|||
|
||||
# Actual Stripe logic.
|
||||
|
||||
def public_api_key():
|
||||
return uncloud.settings.STRIPE_PUBLIC_KEY
|
||||
|
||||
def get_customer_id_for(user):
|
||||
try:
|
||||
# .get() raise if there is no matching entry.
|
||||
|
@ -99,15 +106,17 @@ def get_customer_id_for(user):
|
|||
return None
|
||||
|
||||
@handle_stripe_error
|
||||
def create_card(customer_id, credit_card):
|
||||
return stripe.Customer.create_source(
|
||||
customer_id,
|
||||
card={
|
||||
'number': credit_card.number,
|
||||
'exp_month': credit_card.exp_month,
|
||||
'exp_year': credit_card.exp_year,
|
||||
'cvc': credit_card.cvc
|
||||
})
|
||||
def create_setup_intent(customer_id):
|
||||
return stripe.SetupIntent.create(customer=customer_id)
|
||||
|
||||
@handle_stripe_error
|
||||
def get_setup_intent(setup_intent_id):
|
||||
return stripe.SetupIntent.retrieve(setup_intent_id)
|
||||
|
||||
def get_payment_method(payment_method_id):
|
||||
return stripe.PaymentMethod.retrieve(payment_method_id)
|
||||
|
||||
## Legacy
|
||||
|
||||
@handle_stripe_error
|
||||
def get_card(customer_id, card_id):
|
||||
|
@ -116,13 +125,16 @@ def get_card(customer_id, card_id):
|
|||
@handle_stripe_error
|
||||
def charge_customer(amount, customer_id, card_id):
|
||||
# Amount is in CHF but stripes requires smallest possible unit.
|
||||
# See https://stripe.com/docs/api/charges/create
|
||||
# https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount
|
||||
adjusted_amount = int(amount * 100)
|
||||
return stripe.Charge.create(
|
||||
return stripe.PaymentIntent.create(
|
||||
amount=adjusted_amount,
|
||||
currency=CURRENCY,
|
||||
customer=customer_id,
|
||||
source=card_id)
|
||||
payment_method=card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
)
|
||||
|
||||
@handle_stripe_error
|
||||
def create_customer(name, email):
|
||||
|
|
76
uncloud/uncloud_pay/templates/stripe-payment.html.j2
Normal file
76
uncloud/uncloud_pay/templates/stripe-payment.html.j2
Normal file
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Stripe Card Registration</title>
|
||||
|
||||
<!-- https://stripe.com/docs/js/appendix/viewport_meta_requirements -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<style>
|
||||
#content {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#callback-form {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1>Registering Stripe Credit Card</h1>
|
||||
|
||||
<!-- Stripe form and messages -->
|
||||
<span id="message"></span>
|
||||
<form id="setup-form">
|
||||
<div id="card-element"></div>
|
||||
<button type='button' id="card-button">
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Dirty hack used for callback to API -->
|
||||
<form id="callback-form" action="{{ callback }}" method="post"></form>
|
||||
</div>
|
||||
|
||||
<!-- Enable Stripe from UI elements -->
|
||||
<script>
|
||||
var stripe = Stripe('{{ stripe_pk }}');
|
||||
|
||||
var elements = stripe.elements();
|
||||
var cardElement = elements.create('card');
|
||||
cardElement.mount('#card-element');
|
||||
</script>
|
||||
|
||||
<!-- Handle card submission -->
|
||||
<script>
|
||||
var cardButton = document.getElementById('card-button');
|
||||
var messageContainer = document.getElementById('message');
|
||||
var clientSecret = '{{ client_secret }}';
|
||||
|
||||
cardButton.addEventListener('click', function(ev) {
|
||||
|
||||
stripe.confirmCardSetup(
|
||||
clientSecret,
|
||||
{
|
||||
payment_method: {
|
||||
card: cardElement,
|
||||
billing_details: {
|
||||
},
|
||||
},
|
||||
}
|
||||
).then(function(result) {
|
||||
if (result.error) {
|
||||
var message = document.createTextNode('Error:' + result.error.message);
|
||||
messageContainer.appendChild(message);
|
||||
} else {
|
||||
// Return to API on success.
|
||||
document.getElementById("callback-form").submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +1,12 @@
|
|||
from django.shortcuts import render
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework import viewsets, permissions, status, views
|
||||
from rest_framework.renderers import TemplateHTMLRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.decorators import renderer_classes
|
||||
|
||||
import json
|
||||
|
||||
|
@ -66,7 +69,6 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
|||
else:
|
||||
return PaymentMethodSerializer
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
return PaymentMethod.objects.filter(owner=self.request.user)
|
||||
|
||||
|
@ -75,7 +77,9 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
|||
def create(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
payment_method = PaymentMethod.objects.create(owner=request.user, **serializer.validated_data)
|
||||
|
||||
if serializer.validated_data['source'] == "stripe":
|
||||
# Retrieve Stripe customer ID for user.
|
||||
customer_id = uncloud_stripe.get_customer_id_for(request.user)
|
||||
if customer_id == None:
|
||||
|
@ -83,21 +87,22 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
|||
{'error': 'Could not resolve customer stripe ID.'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# Register card under stripe customer.
|
||||
credit_card = uncloud_stripe.CreditCard(**serializer.validated_data.pop('credit_card'))
|
||||
card_request = uncloud_stripe.create_card(customer_id, credit_card)
|
||||
if card_request['error']:
|
||||
return Response({'stripe_error': card_request['error']}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
card_id = card_request['response_object']['id']
|
||||
# TODO: handle error
|
||||
setup_intent = uncloud_stripe.create_setup_intent(customer_id)
|
||||
payment_method = PaymentMethod.objects.create(
|
||||
owner=request.user,
|
||||
stripe_setup_intent_id=setup_intent['response_object']['id'],
|
||||
**serializer.validated_data)
|
||||
|
||||
# Save payment method locally.
|
||||
serializer.validated_data['stripe_card_id'] = card_request['response_object']['id']
|
||||
payment_method = PaymentMethod.objects.create(owner=request.user, **serializer.validated_data)
|
||||
# TODO: find a way to use reverse properly:
|
||||
# https://www.django-rest-framework.org/api-guide/reverse/
|
||||
query= "payment-method/{}/register-stripe-cc".format(
|
||||
payment_method.uuid
|
||||
)
|
||||
stripe_registration_url = reverse('api-root', request=request) + query
|
||||
return Response({'please_visit': stripe_registration_url})
|
||||
|
||||
# We do not want to return the credit card details sent with the POST
|
||||
# request.
|
||||
output_serializer = PaymentMethodSerializer(payment_method)
|
||||
return Response(output_serializer.data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def charge(self, request, pk=None):
|
||||
|
@ -112,6 +117,39 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
|||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='register-stripe-cc', renderer_classes=[TemplateHTMLRenderer])
|
||||
def register_stripe_cc(self, request, pk=None):
|
||||
payment_method = self.get_object()
|
||||
setup_intent = uncloud_stripe.get_setup_intent(
|
||||
payment_method.stripe_setup_intent_id)
|
||||
|
||||
# Render stripe card registration form.
|
||||
template_args = {
|
||||
'client_secret': setup_intent["response_object"]["client_secret"],
|
||||
'stripe_pk': uncloud_stripe.public_api_key
|
||||
}
|
||||
return Response(template_args, template_name='stripe-payment.html.j2')
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='register-stripe-cc')
|
||||
def register_stripe_cc(self, request, pk=None):
|
||||
payment_method = self.get_object()
|
||||
setup_intent = uncloud_stripe.get_setup_intent(
|
||||
payment_method.stripe_setup_intent_id)
|
||||
|
||||
# Card had been registered, fetching payment method.
|
||||
payment_method_id = setup_intent["response_object"].payment_method
|
||||
if payment_method_id:
|
||||
payment_method.stripe_payment_method_id = payment_method_id
|
||||
payment_method.save()
|
||||
|
||||
return Response({
|
||||
'uuid': payment_method.uuid,
|
||||
'activated': payment_method.active})
|
||||
else:
|
||||
error = 'Could not fetch payment method from stripe. Please try again.'
|
||||
return Response({'error': error})
|
||||
|
||||
|
||||
###
|
||||
# Admin views.
|
||||
|
||||
|
|
Loading…
Reference in a new issue