Merge branch 'sync-old-meowpay-patches' into 'master'
Revamped payment pipeline (imported from nico/mew-cloud) See merge request uncloud/uncloud!3
This commit is contained in:
		
				commit
				
					
						d6bdf5c991
					
				
			
		
					 10 changed files with 356 additions and 148 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,3 @@
 | 
			
		|||
# Live/test key from stripe
 | 
			
		||||
STRIPE_KEY = ''
 | 
			
		||||
 | 
			
		||||
# XML-RPC interface of opennebula
 | 
			
		||||
OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +12,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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,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')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# admin/staff urls
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-03-05 15:24
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('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.AlterUniqueTogether(
 | 
			
		||||
            name='paymentmethod',
 | 
			
		||||
            unique_together=set(),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -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,57 +104,69 @@ 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_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 not self.active:
 | 
			
		||||
            raise Exception('This payment method is inactive.')
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
                charge_request = uncloud_pay.stripe.charge_customer(amount, stripe_customer, self.stripe_card_id)
 | 
			
		||||
                if charge_request['error'] == None:
 | 
			
		||||
                    payment = Payment(owner=self.owner, source=self.source, amount=amount)
 | 
			
		||||
                    payment.save() # TODO: Check return status
 | 
			
		||||
            stripe_payment = uncloud_pay.stripe.charge_customer(
 | 
			
		||||
                    amount, stripe_customer, self.stripe_payment_method_id)
 | 
			
		||||
            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 error: {}'.format(charge_request['error']))
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('This payment method is unsupported/cannot be charged.')
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('Cannot charge negative amount.')
 | 
			
		||||
 | 
			
		||||
    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.primary:
 | 
			
		||||
                return method
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = [['owner', 'primary']]
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Bills & Payments.
 | 
			
		||||
# Bills.
 | 
			
		||||
 | 
			
		||||
class Bill(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,30 +8,29 @@ from .models import *
 | 
			
		|||
class PaymentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Payment
 | 
			
		||||
        fields = ['owner', 'amount', 'source', 'timestamp']
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
class PaymentMethodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    stripe_card_last4 = serializers.IntegerField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4']
 | 
			
		||||
        fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active']
 | 
			
		||||
 | 
			
		||||
class UpdatePaymentMethodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['description']
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
    credit_card = CreditCardSerializer()
 | 
			
		||||
 | 
			
		||||
    please_visit = serializers.CharField(read_only=True)
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['source', 'description', 'primary', 'credit_card']
 | 
			
		||||
        fields = ['uuid', 'primary', 'source', 'description', 'please_visit']
 | 
			
		||||
        read_only_field = ['uuid', 'primary']
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Orders & Products.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,14 @@ 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.
 | 
			
		||||
# Catch errors that should not be displayed to the end user, raise again.
 | 
			
		||||
def handle_stripe_error(f):
 | 
			
		||||
    def handle_problems(*args, **kwargs):
 | 
			
		||||
        response = {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,108 +26,84 @@ 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
 | 
			
		||||
 | 
			
		||||
# 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():
 | 
			
		||||
    return uncloud.secrets.STRIPE_PUBLIC_KEY
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
        try:
 | 
			
		||||
            customer = create_customer(user.username, user.email)
 | 
			
		||||
            uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create(
 | 
			
		||||
                    owner=user, stripe_id=customer.id)
 | 
			
		||||
            return uncloud_stripe_mapping.stripe_id
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            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_card(customer_id, card_id):
 | 
			
		||||
    return stripe.Customer.retrieve_source(customer_id, card_id)
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
@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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
	<head>
 | 
			
		||||
		<title>Error</title>
 | 
			
		||||
		<style>
 | 
			
		||||
			#content {
 | 
			
		||||
				width: 400px;
 | 
			
		||||
				margin: auto;
 | 
			
		||||
			}
 | 
			
		||||
		</style>
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		<div id="content">
 | 
			
		||||
			<h1>Error</h1>
 | 
			
		||||
			<p>{{ error }}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -13,26 +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)
 | 
			
		||||
# Payments and Payment Methods.
 | 
			
		||||
 | 
			
		||||
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
    serializer_class = PaymentSerializer
 | 
			
		||||
| 
						 | 
				
			
			@ -48,19 +32,19 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		|||
    def get_queryset(self):
 | 
			
		||||
        return Order.objects.filter(owner=self.request.user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PaymentMethodViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = [permissions.IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
        if self.action == 'create':
 | 
			
		||||
            return CreatePaymentMethodSerializer
 | 
			
		||||
        elif self.action == 'update':
 | 
			
		||||
            return UpdatePaymentMethodSerializer
 | 
			
		||||
        elif self.action == 'charge':
 | 
			
		||||
            return ChargePaymentMethodSerializer
 | 
			
		||||
        else:
 | 
			
		||||
            return PaymentMethodSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return PaymentMethod.objects.filter(owner=self.request.user)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +54,11 @@ 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)
 | 
			
		||||
            if customer_id == None:
 | 
			
		||||
| 
						 | 
				
			
			@ -77,21 +66,26 @@ 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']
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
            payment_method = PaymentMethod.objects.create(
 | 
			
		||||
                    owner=request.user,
 | 
			
		||||
                    stripe_setup_intent_id=setup_intent.id,
 | 
			
		||||
                    **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 use reverse properly:
 | 
			
		||||
            # https://www.django-rest-framework.org/api-guide/reverse/
 | 
			
		||||
            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})
 | 
			
		||||
        else:
 | 
			
		||||
           serializer.save(owner=request.user, **serializer.validated_data)
 | 
			
		||||
           return Response(serializer.data)
 | 
			
		||||
 | 
			
		||||
    @action(detail=True, methods=['post'])
 | 
			
		||||
    def charge(self, request, pk=None):
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +100,96 @@ 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()
 | 
			
		||||
 | 
			
		||||
        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.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='activate-stripe-cc')
 | 
			
		||||
    def activate_stripe_cc(self, request, pk=None):
 | 
			
		||||
        payment_method = self.get_object()
 | 
			
		||||
        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.
 | 
			
		||||
        print(setup_intent)
 | 
			
		||||
        if setup_intent.payment_method:
 | 
			
		||||
            payment_method.stripe_payment_method_id = setup_intent.payment_method
 | 
			
		||||
            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})
 | 
			
		||||
 | 
			
		||||
    @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)
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Admin views.
 | 
			
		||||
# 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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Old admin stuff.
 | 
			
		||||
 | 
			
		||||
class AdminPaymentViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = PaymentSerializer
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue