Properly wire stripe card to payment methods
This commit is contained in:
parent
3846e49395
commit
94a39ed81d
6 changed files with 117 additions and 43 deletions
24
uncloud/uncloud_pay/migrations/0015_stripecustomer.py
Normal file
24
uncloud/uncloud_pay/migrations/0015_stripecustomer.py
Normal file
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
25
uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py
Normal file
25
uncloud/uncloud_pay/migrations/0016_auto_20200303_1552.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,6 +7,7 @@ from django.utils import timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
import uncloud_pay.stripe
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -68,6 +69,20 @@ class PaymentMethod(models.Model):
|
||||||
# Only used for "Stripe" source
|
# Only used for "Stripe" source
|
||||||
stripe_card_id = models.CharField(max_length=32, blank=True, null=True)
|
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):
|
def charge(self, amount):
|
||||||
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...
|
||||||
if self.source == 'stripe':
|
if self.source == 'stripe':
|
||||||
|
@ -85,6 +100,11 @@ class PaymentMethod(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [['owner', 'primary']]
|
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.
|
# Bills & Payments.
|
||||||
|
|
|
@ -34,9 +34,11 @@ class PaymentSerializer(serializers.ModelSerializer):
|
||||||
fields = ['owner', 'amount', 'source', 'timestamp']
|
fields = ['owner', 'amount', 'source', 'timestamp']
|
||||||
|
|
||||||
class PaymentMethodSerializer(serializers.ModelSerializer):
|
class PaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
|
stripe_card_last4 = serializers.IntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaymentMethod
|
model = PaymentMethod
|
||||||
fields = ['source', 'description', 'primary']
|
fields = ['source', 'description', 'primary', 'stripe_card_last4']
|
||||||
|
|
||||||
class CreditCardSerializer(serializers.Serializer):
|
class CreditCardSerializer(serializers.Serializer):
|
||||||
number = serializers.IntegerField()
|
number = serializers.IntegerField()
|
||||||
|
@ -51,41 +53,6 @@ class CreatePaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
model = PaymentMethod
|
model = PaymentMethod
|
||||||
fields = ['source', 'description', 'primary', 'credit_card']
|
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
|
# Bills
|
||||||
|
|
|
@ -2,6 +2,9 @@ import stripe
|
||||||
import stripe.error
|
import stripe.error
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
import uncloud_pay.models
|
||||||
|
|
||||||
import uncloud.secrets
|
import uncloud.secrets
|
||||||
|
|
||||||
# Static stripe configuration used below.
|
# Static stripe configuration used below.
|
||||||
|
@ -79,11 +82,24 @@ class CreditCard():
|
||||||
|
|
||||||
# Actual Stripe logic.
|
# 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
|
@handle_stripe_error
|
||||||
def create_card(customer_id, credit_card):
|
def create_card(customer_id, credit_card):
|
||||||
# Test settings
|
|
||||||
credit_card.number = "5555555555554444"
|
|
||||||
|
|
||||||
return stripe.Customer.create_source(
|
return stripe.Customer.create_source(
|
||||||
customer_id,
|
customer_id,
|
||||||
card={
|
card={
|
||||||
|
@ -95,7 +111,7 @@ def create_card(customer_id, credit_card):
|
||||||
|
|
||||||
@handle_stripe_error
|
@handle_stripe_error
|
||||||
def get_card(customer_id, card_id):
|
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
|
@handle_stripe_error
|
||||||
def charge_customer(amount, source):
|
def charge_customer(amount, source):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.db import transaction
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import viewsets, permissions, status
|
from rest_framework import viewsets, permissions, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -69,13 +70,34 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return PaymentMethod.objects.filter(owner=self.request.user)
|
return PaymentMethod.objects.filter(owner=self.request.user)
|
||||||
|
|
||||||
|
# XXX: Handling of errors is far from great down there.
|
||||||
|
@transaction.atomic
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save(owner=request.user)
|
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
# Retrieve Stripe customer ID for user.
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
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.
|
# TODO: find a way to customize serializer for actions.
|
||||||
# drf-action-serializer module seems to do that.
|
# drf-action-serializer module seems to do that.
|
||||||
|
|
Loading…
Reference in a new issue