From 94a39ed81de5094cca2d4f6afac4c5d4700ef54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 3 Mar 2020 16:55:56 +0100 Subject: [PATCH] Properly wire stripe card to payment methods --- .../migrations/0015_stripecustomer.py | 24 ++++++++++++ .../migrations/0016_auto_20200303_1552.py | 25 ++++++++++++ uncloud/uncloud_pay/models.py | 20 ++++++++++ uncloud/uncloud_pay/serializers.py | 39 ++----------------- uncloud/uncloud_pay/stripe.py | 24 ++++++++++-- uncloud/uncloud_pay/views.py | 28 +++++++++++-- 6 files changed, 117 insertions(+), 43 deletions(-) create mode 100644 uncloud/uncloud_pay/migrations/0015_stripecustomer.py create mode 100644 uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py diff --git a/uncloud/uncloud_pay/migrations/0015_stripecustomer.py b/uncloud/uncloud_pay/migrations/0015_stripecustomer.py new file mode 100644 index 0000000..14fdbf0 --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0015_stripecustomer.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.3 on 2020-03-03 13:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0014_auto_20200303_1027'), + ] + + operations = [ + migrations.CreateModel( + name='StripeCustomer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=32)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py b/uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py new file mode 100644 index 0000000..08e3f2f --- /dev/null +++ b/uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.3 on 2020-03-03 15:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0015_stripecustomer'), + ] + + operations = [ + migrations.RemoveField( + model_name='stripecustomer', + name='id', + ), + migrations.AlterField( + model_name='stripecustomer', + name='owner', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 62fa098..fa775fc 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -7,6 +7,7 @@ from django.utils import timezone from math import ceil from datetime import timedelta from calendar import monthrange +import uncloud_pay.stripe from decimal import Decimal import uuid @@ -68,6 +69,20 @@ class PaymentMethod(models.Model): # Only used for "Stripe" source stripe_card_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 + else: + return None + + def charge(self, amount): if amount > 0: # Make sure we don't charge negative amount by errors... if self.source == 'stripe': @@ -85,6 +100,11 @@ class PaymentMethod(models.Model): class Meta: unique_together = [['owner', 'primary']] +class StripeCustomer(models.Model): + owner = models.OneToOneField( get_user_model(), + primary_key=True, + on_delete=models.CASCADE) + stripe_id = models.CharField(max_length=32) ### # Bills & Payments. diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index 16b725a..f5136f6 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -34,9 +34,11 @@ class PaymentSerializer(serializers.ModelSerializer): fields = ['owner', 'amount', 'source', 'timestamp'] class PaymentMethodSerializer(serializers.ModelSerializer): + stripe_card_last4 = serializers.IntegerField() + class Meta: model = PaymentMethod - fields = ['source', 'description', 'primary'] + fields = ['source', 'description', 'primary', 'stripe_card_last4'] class CreditCardSerializer(serializers.Serializer): number = serializers.IntegerField() @@ -51,41 +53,6 @@ class CreatePaymentMethodSerializer(serializers.ModelSerializer): model = PaymentMethod fields = ['source', 'description', 'primary', 'credit_card'] - def create(self, validated_data): - credit_card = stripe.CreditCard(**validated_data.pop('credit_card')) - user = self.context['request'].user - customer = stripe.create_customer(user.username, user.email) - # TODO check customer error - customer_id = customer['response_object']['id'] - stripe_card = stripe.create_card(customer_id, credit_card) - # TODO: check credit card error - validated_data['stripe_card_id'] = stripe_card['response_object']['id'] -class CreditCardSerializer(serializers.Serializer): - number = serializers.IntegerField() - exp_month = serializers.IntegerField() - exp_year = serializers.IntegerField() - cvc = serializers.IntegerField() - -class CreatePaymentMethodSerializer(serializers.ModelSerializer): - credit_card = CreditCardSerializer() - - class Meta: - model = PaymentMethod - fields = ['source', 'description', 'primary', 'credit_card'] - - def create(self, validated_data): - credit_card = stripe.CreditCard(**validated_data.pop('credit_card')) - user = self.context['request'].user - customer = stripe.create_customer(user.username, user.email) - # TODO check customer error - customer_id = customer['response_object']['id'] - stripe_card = stripe.create_card(customer_id, credit_card) - # TODO: check credit card error - validated_data['stripe_card_id'] = stripe_card['response_object']['id'] - payment_method = PaymentMethod.objects.create(**validated_data) - return payment_method - payment_method = PaymentMethod.objects.create(**validated_data) - return payment_method ### # Bills diff --git a/uncloud/uncloud_pay/stripe.py b/uncloud/uncloud_pay/stripe.py index c50317f..ab2d865 100644 --- a/uncloud/uncloud_pay/stripe.py +++ b/uncloud/uncloud_pay/stripe.py @@ -2,6 +2,9 @@ import stripe import stripe.error import logging +from django.core.exceptions import ObjectDoesNotExist +import uncloud_pay.models + import uncloud.secrets # Static stripe configuration used below. @@ -79,11 +82,24 @@ class CreditCard(): # Actual Stripe logic. +def get_customer_id_for(user): + try: + # .get() raise if there is no matching entry. + 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( + owner=user, + stripe_id=customer_request['response_object']['id'] + ) + return mapping.stripe_id + else: + return None + @handle_stripe_error def create_card(customer_id, credit_card): - # Test settings - credit_card.number = "5555555555554444" - return stripe.Customer.create_source( customer_id, card={ @@ -95,7 +111,7 @@ def create_card(customer_id, credit_card): @handle_stripe_error def get_card(customer_id, card_id): - return stripe.Card.retrieve_source(customer_id, card_id) + return stripe.Customer.retrieve_source(customer_id, card_id) @handle_stripe_error def charge_customer(amount, source): diff --git a/uncloud/uncloud_pay/views.py b/uncloud/uncloud_pay/views.py index 936d4c7..294b518 100644 --- a/uncloud/uncloud_pay/views.py +++ b/uncloud/uncloud_pay/views.py @@ -1,4 +1,5 @@ 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.response import Response @@ -69,13 +70,34 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): def get_queryset(self): return PaymentMethod.objects.filter(owner=self.request.user) + # XXX: Handling of errors is far from great down there. + @transaction.atomic def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save(owner=request.user) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + # Retrieve Stripe customer ID for user. + customer_id = 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 = stripe.CreditCard(**serializer.validated_data.pop('credit_card')) + card_request = 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'] + + # 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) + + # 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) # TODO: find a way to customize serializer for actions. # drf-action-serializer module seems to do that.