From 929211162dc2c9ea930ea900e96a0f2c5b5a867b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 10:23:34 +0100 Subject: [PATCH 01/17] Replace legacy Stripe Charge API by Payment{setup, intent} --- uncloud/uncloud/urls.py | 1 - .../migrations/0017_auto_20200304_1723.py | 17 +++++ .../migrations/0018_auto_20200305_0819.py | 13 ++++ .../migrations/0019_auto_20200305_0851.py | 23 ++++++ .../migrations/0020_auto_20200305_0911.py | 18 +++++ uncloud/uncloud_pay/models.py | 27 ++++--- uncloud/uncloud_pay/serializers.py | 7 +- uncloud/uncloud_pay/stripe.py | 42 ++++++---- .../templates/stripe-payment.html.j2 | 76 +++++++++++++++++++ uncloud/uncloud_pay/views.py | 76 ++++++++++++++----- 10 files changed, 250 insertions(+), 50 deletions(-) create mode 100644 uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py create mode 100644 uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py create mode 100644 uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py create mode 100644 uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py create mode 100644 uncloud/uncloud_pay/templates/stripe-payment.html.j2 diff --git a/uncloud/uncloud/urls.py b/uncloud/uncloud/urls.py index d7ee153..e42bb7e 100644 --- a/uncloud/uncloud/urls.py +++ b/uncloud/uncloud/urls.py @@ -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') diff --git a/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py b/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py new file mode 100644 index 0000000..3321e66 --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py @@ -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(), + ), + ] diff --git a/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py b/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py new file mode 100644 index 0000000..e0f9087 --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py @@ -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 = [ + ] diff --git a/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py b/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py new file mode 100644 index 0000000..d8a7c22 --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py @@ -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), + ), + ] diff --git a/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py b/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py new file mode 100644 index 0000000..9e1b677 --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py @@ -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', + ), + ] diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 43064e4..e209dbb 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -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. diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index aa75fd9..44402b4 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -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'] ### diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index 4f28d94..72399c8 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -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( - amount=adjusted_amount, - currency=CURRENCY, - customer=customer_id, - source=card_id) + return stripe.PaymentIntent.create( + amount=adjusted_amount, + currency=CURRENCY, + customer=customer_id, + payment_method=card_id, + off_session=True, + confirm=True, + ) @handle_stripe_error def create_customer(name, email): diff --git a/uncloud/uncloud_pay/templates/stripe-payment.html.j2 b/uncloud/uncloud_pay/templates/stripe-payment.html.j2 new file mode 100644 index 0000000..6c59740 --- /dev/null +++ b/uncloud/uncloud_pay/templates/stripe-payment.html.j2 @@ -0,0 +1,76 @@ + + + + Stripe Card Registration + + + + + + + + +
+

Registering Stripe Credit Card

+ + + +
+
+ +
+ + +
+
+ + + + + + + + diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index 38d1aa4..32e7238 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -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 @@ -69,7 +72,6 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): else: return PaymentMethodSerializer - def get_queryset(self): return PaymentMethod.objects.filter(owner=self.request.user) @@ -78,29 +80,32 @@ 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) - # Retrieve Stripe customer ID for user. - customer_id = uncloud_stripe.get_customer_id_for(request.user) - if customer_id == None: - return Response( + 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: + return Response( {'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): @@ -115,6 +120,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. From 7e9f2ea5614a9580458ce588810afbe99162a591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 10:27:33 +0100 Subject: [PATCH 02/17] Cleanup/reorder uncloud_pay views --- uncloud/uncloud_pay/models.py | 2 +- uncloud/uncloud_pay/views.py | 71 ++++++++++++++++------------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index e209dbb..8c9fc76 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -171,7 +171,7 @@ class PaymentMethod(models.Model): pass ### -# Bills & Payments. +# Bills. class Bill(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index 32e7238..a22c616 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -16,43 +16,7 @@ from datetime import datetime import uncloud_pay.stripe as uncloud_stripe ### -# Standard user views: - -class BalanceViewSet(viewsets.ViewSet): - # here we return a number - # number = sum(payments) - sum(bills) - - #bills = Bill.objects.filter(owner=self.request.user) - #payments = Payment.objects.filter(owner=self.request.user) - - # sum_paid = sum([ amount for amount payments..,. ]) # you get the picture - # sum_to_be_paid = sum([ amount for amount bills..,. ]) # you get the picture - pass - - -class BillViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = BillSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Bill.objects.filter(owner=self.request.user) - - def unpaid(self, request): - return Bill.objects.filter(owner=self.request.user, paid=False) - -class PaymentViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = PaymentSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Payment.objects.filter(owner=self.request.user) - -class OrderViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = OrderSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Order.objects.filter(owner=self.request.user) +# Users. class UserViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = UserSerializer @@ -61,6 +25,16 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return get_user_model().objects.all() +### +# Payments and Payment Methods. + +class PaymentViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = PaymentSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Payment.objects.filter(owner=self.request.user) + class PaymentMethodViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] @@ -152,9 +126,30 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): error = 'Could not fetch payment method from stripe. Please try again.' return Response({'error': error}) +### +# Bills and Orders. + +class BillViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = BillSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Bill.objects.filter(owner=self.request.user) + + def unpaid(self, request): + return Bill.objects.filter(owner=self.request.user, paid=False) + + +class OrderViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = OrderSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Order.objects.filter(owner=self.request.user) + ### -# Admin views. +# Old admin stuff. class AdminPaymentViewSet(viewsets.ModelViewSet): serializer_class = PaymentSerializer From 08bf7cd3204b9e13c1522a659192143d9582d6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 10:28:03 +0100 Subject: [PATCH 03/17] Add STRIPE_PUBLIC_KEY setting --- uncloud/uncloud/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uncloud/uncloud/settings.py b/uncloud/uncloud/settings.py index cc0ec3a..9f1ac91 100644 --- a/uncloud/uncloud/settings.py +++ b/uncloud/uncloud/settings.py @@ -172,3 +172,7 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' + +################################################################################ +# Stripe +STRIPE_PUBLIC_KEY="" From 4cc19e1e6e5fbf11f3f5c5e1f744eb83b5da78ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 10:28:50 +0100 Subject: [PATCH 04/17] Remove legacy credit card support --- uncloud/uncloud_pay/serializers.py | 6 ------ uncloud/uncloud_pay/stripe.py | 19 ------------------- 2 files changed, 25 deletions(-) diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index 44402b4..d406493 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -34,12 +34,6 @@ class PaymentMethodSerializer(serializers.ModelSerializer): class ChargePaymentMethodSerializer(serializers.Serializer): amount = serializers.DecimalField(max_digits=10, decimal_places=2) -class CreditCardSerializer(serializers.Serializer): - number = serializers.IntegerField() - exp_month = serializers.IntegerField() - exp_year = serializers.IntegerField() - cvc = serializers.IntegerField() - class CreatePaymentMethodSerializer(serializers.ModelSerializer): please_visit = serializers.CharField(read_only=True) class Meta: diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index 72399c8..1d745ef 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -71,19 +71,6 @@ def handle_stripe_error(f): return handle_problems -# Convenience CC container, also used for serialization. -class CreditCard(): - number = None - exp_year = None - exp_month = None - cvc = None - - def __init__(self, number, exp_month, exp_year, cvc): - self.number=number - self.exp_year = exp_year - self.exp_month = exp_month - self.cvc = cvc - # Actual Stripe logic. def public_api_key(): @@ -116,12 +103,6 @@ def get_setup_intent(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): - return stripe.Customer.retrieve_source(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. From 21e1a3d220f4771e2e7e6c59460ee08231d9256b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:03:47 +0100 Subject: [PATCH 05/17] Revamp stripe error handling --- uncloud/uncloud_pay/models.py | 37 +++++++----- uncloud/uncloud_pay/stripe.py | 46 ++++++-------- uncloud/uncloud_pay/templates/error.html.j2 | 18 ++++++ uncloud/uncloud_pay/views.py | 66 +++++++++++++++------ 4 files changed, 104 insertions(+), 63 deletions(-) create mode 100644 uncloud/uncloud_pay/templates/error.html.j2 diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 8c9fc76..c8aba99 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -139,35 +139,40 @@ class PaymentMethod(models.Model): 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_payment_method_id) - if charge_request['error'] == None: - payment = Payment(owner=self.owner, source=self.source, amount=amount) - payment.save() # TODO: Check return status + if not self.active: + raise Exception('This payment method is inactive.') - return payment - else: - raise Exception('Stripe error: {}'.format(charge_request['error'])) - else: - raise Exception('This payment method is unsupported/cannot be charged.') - else: + if amount > 0: # Make sure we don't charge negative amount by errors... raise Exception('Cannot charge negative amount.') + if self.source == 'stripe': + stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id + stripe_payment = uncloud_pay.stripe.charge_customer( + amount, stripe_customer, self.stripe_payment_method_id) + if stripe_payment['paid']: + payment = Payment(owner=self.owner, source=self.source, amount=amount) + payment.save() # TODO: Check return status + + return payment + else: + raise Exception(stripe_payment['error']) + else: + raise Exception('This payment method is unsupported/cannot be charged.') + def get_primary_for(user): methods = PaymentMethod.objects.filter(owner=user) for method in methods: # Do we want to do something with non-primary method? - if method.primary: + if method.active and method.primary: return method return None class Meta: - #API_keyunique_together = [['owner', 'primary']] + # TODO: limit to one primary method per user. + # unique_together is no good since it won't allow more than one + # non-primary method. pass ### diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index 1d745ef..7dc53c6 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -17,6 +17,7 @@ CURRENCY = 'chf' stripe.api_key = uncloud.secrets.STRIPE_KEY # Helper (decorator) used to catch errors raised by stripe logic. +# Catch errors that should not be displayed to the end user, raise again. def handle_stripe_error(f): def handle_problems(*args, **kwargs): response = { @@ -25,49 +26,38 @@ def handle_stripe_error(f): 'error': None } - common_message = "Currently it is not possible to make payments." + common_message = "Currently it is not possible to make payments. Please try agin later." try: response_object = f(*args, **kwargs) - response = { - 'response_object': response_object, - 'error': None - } - return response + return response_object except stripe.error.CardError as e: # Since it's a decline, stripe.error.CardError will be caught body = e.json_body - err = body['error'] - response.update({'error': err['message']}) logging.error(str(e)) - return response + + raise e # For error handling. except stripe.error.RateLimitError: - response.update( - {'error': "Too many requests made to the API too quickly"}) - return response + logging.error("Too many requests made to the API too quickly.") + raise Exception(common_message) except stripe.error.InvalidRequestError as e: logging.error(str(e)) - response.update({'error': "Invalid parameters"}) - return response + raise Exception('Invalid parameters.') except stripe.error.AuthenticationError as e: # Authentication with Stripe's API failed # (maybe you changed API keys recently) logging.error(str(e)) - response.update({'error': common_message}) - return response + raise Exception(common_message) except stripe.error.APIConnectionError as e: logging.error(str(e)) - response.update({'error': common_message}) - return response + raise Exception(common_message) except stripe.error.StripeError as e: - # maybe send email + # XXX: maybe send email logging.error(str(e)) - response.update({'error': common_message}) - return response + raise Exception(common_message) except Exception as e: # maybe send email logging.error(str(e)) - response.update({'error': common_message}) - return response + raise Exception(common_message) return handle_problems @@ -82,14 +72,14 @@ def get_customer_id_for(user): return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id except ObjectDoesNotExist: # No entry yet - making a new one. - customer_request = create_customer(user.username, user.email) - if customer_request['error'] == None: - mapping = uncloud_pay.models.StripeCustomer.objects.create( + try: + customer = create_customer(user.username, user.email) + uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create( owner=user, stripe_id=customer_request['response_object']['id'] ) - return mapping.stripe_id - else: + return uncloud_stripe_mapping.stripe_id + except Exception as e: return None @handle_stripe_error diff --git a/uncloud/uncloud_pay/templates/error.html.j2 b/uncloud/uncloud_pay/templates/error.html.j2 new file mode 100644 index 0000000..ba9209c --- /dev/null +++ b/uncloud/uncloud_pay/templates/error.html.j2 @@ -0,0 +1,18 @@ + + + + Error + + + +
+

Error

+

{{ error }}

+
+ + diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index a22c616..08e94a0 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -58,25 +58,28 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): if serializer.validated_data['source'] == "stripe": # Retrieve Stripe customer ID for user. - customer_id = uncloud_stripe.get_customer_id_for(request.user) + customer_id = uncloud_stripe.get_customer_id_for(request.user) if customer_id == None: return Response( {'error': 'Could not resolve customer stripe ID.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # TODO: handle error - setup_intent = uncloud_stripe.create_setup_intent(customer_id) + try: + setup_intent = uncloud_stripe.create_setup_intent(customer_id) + except Exception as e: + return Response({'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + payment_method = PaymentMethod.objects.create( owner=request.user, - stripe_setup_intent_id=setup_intent['response_object']['id'], + stripe_setup_intent_id=setup_intent.id, **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 + path = "payment-method/{}/register-stripe-cc".format( + payment_method.uuid) + stripe_registration_url = reverse('api-root', request=request) + path return Response({'please_visit': stripe_registration_url}) return Response(serializer.data) @@ -97,26 +100,51 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): @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) + if payment_method.source != 'stripe': + return Response( + {'error': 'This is not a Stripe-based payment method.'}, + template_name='error.html.j2') + + if payment_method.active: + return Response( + {'error': 'This payment method is already active'}, + template_name='error.html.j2') + + try: + setup_intent = uncloud_stripe.get_setup_intent( + payment_method.stripe_setup_intent_id) + except Exception as e: + return Response( + {'error': str(e)}, + template_name='error.html.j2') + + # TODO: find a way to use reverse properly: + # https://www.django-rest-framework.org/api-guide/reverse/ + callback_path= "payment-method/{}/activate-stripe-cc/".format( + payment_method.uuid) + callback = reverse('api-root', request=request) + callback_path # Render stripe card registration form. template_args = { - 'client_secret': setup_intent["response_object"]["client_secret"], - 'stripe_pk': uncloud_stripe.public_api_key + 'client_secret': setup_intent.client_secret, + 'stripe_pk': uncloud_stripe.public_api_key, + 'callback': callback } 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): + @action(detail=True, methods=['post'], url_path='activate-stripe-cc') + def activate_stripe_cc(self, request, pk=None): payment_method = self.get_object() - setup_intent = uncloud_stripe.get_setup_intent( - payment_method.stripe_setup_intent_id) + try: + setup_intent = uncloud_stripe.get_setup_intent( + payment_method.stripe_setup_intent_id) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 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 + print(setup_intent) + if setup_intent.payment_method: + payment_method.stripe_payment_method_id = setup_intent.payment_method payment_method.save() return Response({ From 2f70418f4d6f9b6393611a563ccc339f4abc1838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:13:04 +0100 Subject: [PATCH 06/17] Fix dumb logic errors/typo from last commit --- uncloud/uncloud_pay/models.py | 13 +++++++------ uncloud/uncloud_pay/serializers.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index c8aba99..0ac4107 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -142,20 +142,21 @@ class PaymentMethod(models.Model): if not self.active: raise Exception('This payment method is inactive.') - if amount > 0: # Make sure we don't charge negative amount by errors... + if amount < 0: # Make sure we don't charge negative amount by errors... raise Exception('Cannot charge negative amount.') if self.source == 'stripe': stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id stripe_payment = uncloud_pay.stripe.charge_customer( amount, stripe_customer, self.stripe_payment_method_id) - if stripe_payment['paid']: - payment = Payment(owner=self.owner, source=self.source, amount=amount) - payment.save() # TODO: Check return status + print(stripe_payment) + if 'paid' in stripe_payment and stripe_payment['paid'] == False: + raise Exception(stripe_payment['error']) + else: + payment = Payment.objects.create( + owner=self.owner, source=self.source, amount=amount) return payment - else: - raise Exception(stripe_payment['error']) else: raise Exception('This payment method is unsupported/cannot be charged.') diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index d406493..bfbe0da 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -22,7 +22,7 @@ class UserSerializer(serializers.ModelSerializer): class PaymentSerializer(serializers.ModelSerializer): class Meta: model = Payment - fields = ['owner', 'amount', 'source', 'timestamp'] + fields = '__all__' class PaymentMethodSerializer(serializers.ModelSerializer): stripe_card_last4 = serializers.IntegerField() From a41184d83d23d8344060c881c282e452d7407653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:27:43 +0100 Subject: [PATCH 07/17] Fix generate-bills, remove debug print in charge method --- uncloud/uncloud_pay/management/commands/generate-bills.py | 5 +---- uncloud/uncloud_pay/models.py | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/uncloud/uncloud_pay/management/commands/generate-bills.py b/uncloud/uncloud_pay/management/commands/generate-bills.py index a7dbe78..5bd4519 100644 --- a/uncloud/uncloud_pay/management/commands/generate-bills.py +++ b/uncloud/uncloud_pay/management/commands/generate-bills.py @@ -9,8 +9,6 @@ from datetime import timedelta, date from django.utils import timezone from uncloud_pay.models import Bill -BILL_PAYMENT_DELAY=timedelta(days=10) - logger = logging.getLogger(__name__) class Command(BaseCommand): @@ -31,8 +29,7 @@ class Command(BaseCommand): Bill.generate_for( year=now.year, month=now.month, - user=user, - allowed_delay=BILL_PAYMENT_DELAY) + user=user) # We're done for this round :-) print("=> Done.") diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 0ac4107..ac91034 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -149,7 +149,6 @@ class PaymentMethod(models.Model): stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id stripe_payment = uncloud_pay.stripe.charge_customer( amount, stripe_customer, self.stripe_payment_method_id) - print(stripe_payment) if 'paid' in stripe_payment and stripe_payment['paid'] == False: raise Exception(stripe_payment['error']) else: From d6ee806467d5b19478b1bf06528dc32b1566bcd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:28:20 +0100 Subject: [PATCH 08/17] Fix error in stripe get_customer_id_for --- uncloud/uncloud_pay/stripe.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index 7dc53c6..ab3eac2 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -75,9 +75,7 @@ def get_customer_id_for(user): try: customer = create_customer(user.username, user.email) uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create( - owner=user, - stripe_id=customer_request['response_object']['id'] - ) + owner=user, stripe_id=customer.id) return uncloud_stripe_mapping.stripe_id except Exception as e: return None From b88dfa4bfe63cd5ff35e8d34d72f3588df367d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:28:43 +0100 Subject: [PATCH 09/17] Fix payment update updates --- uncloud/uncloud_pay/serializers.py | 5 +++++ uncloud/uncloud_pay/views.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index bfbe0da..46ceab2 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -31,6 +31,11 @@ class PaymentMethodSerializer(serializers.ModelSerializer): model = PaymentMethod fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active'] +class UpdatePaymentMethodSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethod + fields = ['description', 'primary'] + class ChargePaymentMethodSerializer(serializers.Serializer): amount = serializers.DecimalField(max_digits=10, decimal_places=2) diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index 08e94a0..6b54214 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -41,6 +41,8 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.action == 'create': return CreatePaymentMethodSerializer + elif self.action == 'update': + return UpdatePaymentMethodSerializer elif self.action == 'charge': return ChargePaymentMethodSerializer else: From 546667d117b7e18780c7476bc82856b730389e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:36:19 +0100 Subject: [PATCH 10/17] Remove unused empty migration --- .../migrations/0018_auto_20200305_0819.py | 13 ------------- .../migrations/0019_auto_20200305_0851.py | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py diff --git a/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py b/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py deleted file mode 100644 index e0f9087..0000000 --- a/uncloud/uncloud_pay/migrations/0018_auto_20200305_0819.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 = [ - ] diff --git a/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py b/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py index d8a7c22..f8b56cc 100644 --- a/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py +++ b/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0018_auto_20200305_0819'), + ('uncloud_pay', '0017_auto_20200304_1723'), ] operations = [ From e9b6a6f27771e4195cc66f843e3e0d319bd99ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:43:07 +0100 Subject: [PATCH 11/17] Fix migration dependencies after rebase --- uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py b/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py index 3321e66..48142e4 100644 --- a/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py +++ b/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py @@ -6,7 +6,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0016_auto_20200303_1552'), + ('uncloud_pay', '0001_initial'), ] operations = [ From b958cc77ead642c7cac2665a2d64e8771293bd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:45:37 +0100 Subject: [PATCH 12/17] Fix duplicates in payment method creation --- uncloud/uncloud_pay/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index 6b54214..32350ff 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -56,7 +56,6 @@ 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. @@ -83,8 +82,9 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): payment_method.uuid) stripe_registration_url = reverse('api-root', request=request) + path return Response({'please_visit': stripe_registration_url}) - - return Response(serializer.data) + else: + serializer.save(owner=request.user, **serializer.validated_data) + return Response(serializer.data) @action(detail=True, methods=['post']) def charge(self, request, pk=None): From b07df26eb26a1faa6d3098cdc5c8c89982682aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 11:51:08 +0100 Subject: [PATCH 13/17] Move STRIPE_PUBLIC_KEY to secrets (i.e. local configuration) --- uncloud/uncloud/secrets_sample.py | 3 ++- uncloud/uncloud/settings.py | 4 ---- uncloud/uncloud_pay/stripe.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/uncloud/uncloud/secrets_sample.py b/uncloud/uncloud/secrets_sample.py index 464662f..bc9cd38 100644 --- a/uncloud/uncloud/secrets_sample.py +++ b/uncloud/uncloud/secrets_sample.py @@ -15,6 +15,7 @@ LDAP_ADMIN_PASSWORD="" LDAP_SERVER_URI = "" # Stripe (Credit Card payments) -STRIPE_API_key="" +STRIPE_KEY="" +STRIPE_PUBLIC_KEY="" SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf" diff --git a/uncloud/uncloud/settings.py b/uncloud/uncloud/settings.py index 9f1ac91..cc0ec3a 100644 --- a/uncloud/uncloud/settings.py +++ b/uncloud/uncloud/settings.py @@ -172,7 +172,3 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' - -################################################################################ -# Stripe -STRIPE_PUBLIC_KEY="" diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index ab3eac2..f23002b 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -64,7 +64,7 @@ def handle_stripe_error(f): # Actual Stripe logic. def public_api_key(): - return uncloud.settings.STRIPE_PUBLIC_KEY + return uncloud.secrets.STRIPE_PUBLIC_KEY def get_customer_id_for(user): try: From 6c7f0e98b3b6721bdb54d81806c68af3b8abfbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 16:24:45 +0100 Subject: [PATCH 14/17] Rebuild paymentmethod/stripe migrations from master --- ...0305_0851.py => 0002_auto_20200305_1524.py} | 16 ++++++++++------ .../migrations/0017_auto_20200304_1723.py | 17 ----------------- .../migrations/0020_auto_20200305_0911.py | 18 ------------------ 3 files changed, 10 insertions(+), 41 deletions(-) rename uncloud/uncloud_pay/migrations/{0019_auto_20200305_0851.py => 0002_auto_20200305_1524.py} (53%) delete mode 100644 uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py delete mode 100644 uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py diff --git a/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py b/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py similarity index 53% rename from uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py rename to uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py index f8b56cc..0768dd0 100644 --- a/uncloud/uncloud_pay/migrations/0019_auto_20200305_0851.py +++ b/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.4 on 2020-03-05 08:51 +# Generated by Django 3.0.3 on 2020-03-05 15:24 from django.db import migrations, models @@ -6,18 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0017_auto_20200304_1723'), + ('uncloud_pay', '0001_initial'), ] operations = [ + migrations.RenameField( + model_name='paymentmethod', + old_name='stripe_card_id', + new_name='stripe_payment_method_id', + ), 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), + migrations.AlterUniqueTogether( + name='paymentmethod', + unique_together=set(), ), ] diff --git a/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py b/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py deleted file mode 100644 index 48142e4..0000000 --- a/uncloud/uncloud_pay/migrations/0017_auto_20200304_1723.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.0.4 on 2020-03-04 17:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0001_initial'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='paymentmethod', - unique_together=set(), - ), - ] diff --git a/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py b/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py deleted file mode 100644 index 9e1b677..0000000 --- a/uncloud/uncloud_pay/migrations/0020_auto_20200305_0911.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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', - ), - ] From 89c705f7d205874e9b2f04a28e465259ec9bf4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 15:19:25 +0100 Subject: [PATCH 15/17] Set one payment method as primary, allow updates --- .../migrations/0003_auto_20200305_1354.py | 18 +++++++++++++++ .../uncloud/uncloud_pay/models.py | 22 +++++++++---------- .../uncloud/uncloud_pay/serializers.py | 5 +++-- .../uncloud/uncloud_pay/views.py | 13 +++++++++++ 4 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py new file mode 100644 index 0000000..d99ece7 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-03-05 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0002_auto_20200305_1524.py'), + ] + + operations = [ + migrations.AlterField( + model_name='paymentmethod', + name='primary', + field=models.BooleanField(default=False, editable=False), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 10ae985..f7aee62 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -4,9 +4,7 @@ from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator from django.utils.translation import gettext_lazy as _ from django.utils import timezone -from django.dispatch import receiver from django.core.exceptions import ObjectDoesNotExist -import django.db.models.signals as signals import uuid from functools import reduce @@ -106,7 +104,7 @@ class PaymentMethod(models.Model): ), default='stripe') description = models.TextField() - primary = models.BooleanField(default=True) + primary = models.BooleanField(default=False, editable=False) # Only used for "Stripe" source stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) @@ -149,22 +147,24 @@ class PaymentMethod(models.Model): else: raise Exception('This payment method is unsupported/cannot be charged.') + def set_as_primary_for(self, user): + methods = PaymentMethod.objects.filter(owner=user, primary=True) + for method in methods: + print(method) + method.primary = False + method.save() + + self.primary = True + self.save() def get_primary_for(user): methods = PaymentMethod.objects.filter(owner=user) for method in methods: - # Do we want to do something with non-primary method? - if method.active and method.primary: + if method.primary: return method return None - class Meta: - # TODO: limit to one primary method per user. - # unique_together is no good since it won't allow more than one - # non-primary method. - pass - ### # Bills. diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index f408d1b..72316a6 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -20,7 +20,7 @@ class PaymentMethodSerializer(serializers.ModelSerializer): class UpdatePaymentMethodSerializer(serializers.ModelSerializer): class Meta: model = PaymentMethod - fields = ['description', 'primary'] + fields = ['description'] class ChargePaymentMethodSerializer(serializers.Serializer): amount = serializers.DecimalField(max_digits=10, decimal_places=2) @@ -29,7 +29,8 @@ class CreatePaymentMethodSerializer(serializers.ModelSerializer): please_visit = serializers.CharField(read_only=True) class Meta: model = PaymentMethod - fields = ['source', 'description', 'primary', 'please_visit'] + fields = ['uuid', 'primary', 'source', 'description', 'please_visit'] + read_only_field = ['uuid', 'primary'] ### # Orders & Products. diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 567874d..762a3c0 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -64,6 +64,10 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + # Set newly created method as primary if no other method is. + if PaymentMethod.get_primary_for(request.user) == None: + serializer.validated_data['primary'] = True + if serializer.validated_data['source'] == "stripe": # Retrieve Stripe customer ID for user. customer_id = uncloud_stripe.get_customer_id_for(request.user) @@ -109,6 +113,7 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): @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() + if payment_method.source != 'stripe': return Response( {'error': 'This is not a Stripe-based payment method.'}, @@ -163,6 +168,14 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): error = 'Could not fetch payment method from stripe. Please try again.' return Response({'error': error}) + @action(detail=True, methods=['post'], url_path='set-as-primary') + def set_as_primary(self, request, pk=None): + payment_method = self.get_object() + payment_method.set_as_primary_for(request.user) + + serializer = self.get_serializer(payment_method) + return Response(serializer.data) + ### # Bills and Orders. From a8b81b074b03efab81db42db90a8ab6a11f57b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 8 Apr 2020 17:40:44 +0200 Subject: [PATCH 16/17] Remove user view from uncloud_pay --- uncloud_django_based/uncloud/uncloud_pay/views.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 762a3c0..b64981f 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -15,16 +15,6 @@ from .serializers import * from datetime import datetime import uncloud_pay.stripe as uncloud_stripe -### -# Users. - -class UserViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = UserSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return get_user_model().objects.all() - ### # Payments and Payment Methods. From cc7056c87c943798ed07205ac743f9d4578878c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 8 Apr 2020 17:55:48 +0200 Subject: [PATCH 17/17] Remove old Stripe settings from secrets_sample.py --- uncloud_django_based/uncloud/uncloud/secrets_sample.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud/secrets_sample.py b/uncloud_django_based/uncloud/uncloud/secrets_sample.py index bc9cd38..6b0a556 100644 --- a/uncloud_django_based/uncloud/uncloud/secrets_sample.py +++ b/uncloud_django_based/uncloud/uncloud/secrets_sample.py @@ -1,6 +1,3 @@ -# Live/test key from stripe -STRIPE_KEY = '' - # XML-RPC interface of opennebula OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2'