forked from uncloud/uncloud
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
|
# XML-RPC interface of opennebula
|
||||||
OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2'
|
OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2'
|
||||||
|
|
||||||
|
@ -15,6 +12,7 @@ LDAP_ADMIN_PASSWORD=""
|
||||||
LDAP_SERVER_URI = ""
|
LDAP_SERVER_URI = ""
|
||||||
|
|
||||||
# Stripe (Credit Card payments)
|
# Stripe (Credit Card payments)
|
||||||
STRIPE_API_key=""
|
STRIPE_KEY=""
|
||||||
|
STRIPE_PUBLIC_KEY=""
|
||||||
|
|
||||||
SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf"
|
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'bill', payviews.BillViewSet, basename='bill')
|
||||||
router.register(r'order', payviews.OrderViewSet, basename='order')
|
router.register(r'order', payviews.OrderViewSet, basename='order')
|
||||||
router.register(r'payment', payviews.PaymentViewSet, basename='payment')
|
router.register(r'payment', payviews.PaymentViewSet, basename='payment')
|
||||||
router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-methods')
|
|
||||||
|
|
||||||
|
|
||||||
# admin/staff urls
|
# 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.core.validators import MinValueValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
import django.db.models.signals as signals
|
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
@ -106,57 +104,69 @@ class PaymentMethod(models.Model):
|
||||||
),
|
),
|
||||||
default='stripe')
|
default='stripe')
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
primary = models.BooleanField(default=True)
|
primary = models.BooleanField(default=False, editable=False)
|
||||||
|
|
||||||
# Only used for "Stripe" source
|
# 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
|
@property
|
||||||
def stripe_card_last4(self):
|
def stripe_card_last4(self):
|
||||||
if self.source == 'stripe':
|
if self.source == 'stripe' and self.active:
|
||||||
card_request = uncloud_pay.stripe.get_card(
|
payment_method = uncloud_pay.stripe.get_payment_method(
|
||||||
StripeCustomer.objects.get(owner=self.owner).stripe_id,
|
self.stripe_payment_method_id)
|
||||||
self.stripe_card_id)
|
return payment_method.card.last4
|
||||||
if card_request['error'] == None:
|
|
||||||
return card_request['response_object']['last4']
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
else:
|
||||||
return None
|
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):
|
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':
|
if self.source == 'stripe':
|
||||||
stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
|
stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
|
||||||
charge_request = uncloud_pay.stripe.charge_customer(amount, stripe_customer, self.stripe_card_id)
|
stripe_payment = uncloud_pay.stripe.charge_customer(
|
||||||
if charge_request['error'] == None:
|
amount, stripe_customer, self.stripe_payment_method_id)
|
||||||
payment = Payment(owner=self.owner, source=self.source, amount=amount)
|
if 'paid' in stripe_payment and stripe_payment['paid'] == False:
|
||||||
payment.save() # TODO: Check return status
|
raise Exception(stripe_payment['error'])
|
||||||
|
else:
|
||||||
|
payment = Payment.objects.create(
|
||||||
|
owner=self.owner, source=self.source, amount=amount)
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
else:
|
|
||||||
raise Exception('Stripe error: {}'.format(charge_request['error']))
|
|
||||||
else:
|
else:
|
||||||
raise Exception('This payment method is unsupported/cannot be charged.')
|
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):
|
def get_primary_for(user):
|
||||||
methods = PaymentMethod.objects.filter(owner=user)
|
methods = PaymentMethod.objects.filter(owner=user)
|
||||||
for method in methods:
|
for method in methods:
|
||||||
# Do we want to do something with non-primary method?
|
|
||||||
if method.primary:
|
if method.primary:
|
||||||
return method
|
return method
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = [['owner', 'primary']]
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Bills & Payments.
|
# Bills.
|
||||||
|
|
||||||
class Bill(models.Model):
|
class Bill(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
|
@ -8,30 +8,29 @@ from .models import *
|
||||||
class PaymentSerializer(serializers.ModelSerializer):
|
class PaymentSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Payment
|
model = Payment
|
||||||
fields = ['owner', 'amount', 'source', 'timestamp']
|
fields = '__all__'
|
||||||
|
|
||||||
class PaymentMethodSerializer(serializers.ModelSerializer):
|
class PaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
stripe_card_last4 = serializers.IntegerField()
|
stripe_card_last4 = serializers.IntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaymentMethod
|
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):
|
class ChargePaymentMethodSerializer(serializers.Serializer):
|
||||||
amount = serializers.DecimalField(max_digits=10, decimal_places=2)
|
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):
|
class CreatePaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
credit_card = CreditCardSerializer()
|
please_visit = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaymentMethod
|
model = PaymentMethod
|
||||||
fields = ['source', 'description', 'primary', 'credit_card']
|
fields = ['uuid', 'primary', 'source', 'description', 'please_visit']
|
||||||
|
read_only_field = ['uuid', 'primary']
|
||||||
|
|
||||||
###
|
###
|
||||||
# Orders & Products.
|
# Orders & Products.
|
||||||
|
|
|
@ -10,9 +10,14 @@ import uncloud.secrets
|
||||||
# Static stripe configuration used below.
|
# Static stripe configuration used below.
|
||||||
CURRENCY = 'chf'
|
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
|
stripe.api_key = uncloud.secrets.STRIPE_KEY
|
||||||
|
|
||||||
# Helper (decorator) used to catch errors raised by stripe logic.
|
# 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_stripe_error(f):
|
||||||
def handle_problems(*args, **kwargs):
|
def handle_problems(*args, **kwargs):
|
||||||
response = {
|
response = {
|
||||||
|
@ -21,108 +26,84 @@ def handle_stripe_error(f):
|
||||||
'error': None
|
'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:
|
try:
|
||||||
response_object = f(*args, **kwargs)
|
response_object = f(*args, **kwargs)
|
||||||
response = {
|
return response_object
|
||||||
'response_object': response_object,
|
|
||||||
'error': None
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
except stripe.error.CardError as e:
|
except stripe.error.CardError as e:
|
||||||
# Since it's a decline, stripe.error.CardError will be caught
|
# Since it's a decline, stripe.error.CardError will be caught
|
||||||
body = e.json_body
|
body = e.json_body
|
||||||
err = body['error']
|
|
||||||
response.update({'error': err['message']})
|
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
return response
|
|
||||||
|
raise e # For error handling.
|
||||||
except stripe.error.RateLimitError:
|
except stripe.error.RateLimitError:
|
||||||
response.update(
|
logging.error("Too many requests made to the API too quickly.")
|
||||||
{'error': "Too many requests made to the API too quickly"})
|
raise Exception(common_message)
|
||||||
return response
|
|
||||||
except stripe.error.InvalidRequestError as e:
|
except stripe.error.InvalidRequestError as e:
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
response.update({'error': "Invalid parameters"})
|
raise Exception('Invalid parameters.')
|
||||||
return response
|
|
||||||
except stripe.error.AuthenticationError as e:
|
except stripe.error.AuthenticationError as e:
|
||||||
# Authentication with Stripe's API failed
|
# Authentication with Stripe's API failed
|
||||||
# (maybe you changed API keys recently)
|
# (maybe you changed API keys recently)
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
response.update({'error': common_message})
|
raise Exception(common_message)
|
||||||
return response
|
|
||||||
except stripe.error.APIConnectionError as e:
|
except stripe.error.APIConnectionError as e:
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
response.update({'error': common_message})
|
raise Exception(common_message)
|
||||||
return response
|
|
||||||
except stripe.error.StripeError as e:
|
except stripe.error.StripeError as e:
|
||||||
# maybe send email
|
# XXX: maybe send email
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
response.update({'error': common_message})
|
raise Exception(common_message)
|
||||||
return response
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# maybe send email
|
# maybe send email
|
||||||
logging.error(str(e))
|
logging.error(str(e))
|
||||||
response.update({'error': common_message})
|
raise Exception(common_message)
|
||||||
return response
|
|
||||||
|
|
||||||
return handle_problems
|
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.
|
# Actual Stripe logic.
|
||||||
|
|
||||||
|
def public_api_key():
|
||||||
|
return uncloud.secrets.STRIPE_PUBLIC_KEY
|
||||||
|
|
||||||
def get_customer_id_for(user):
|
def get_customer_id_for(user):
|
||||||
try:
|
try:
|
||||||
# .get() raise if there is no matching entry.
|
# .get() raise if there is no matching entry.
|
||||||
return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id
|
return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
# No entry yet - making a new one.
|
# No entry yet - making a new one.
|
||||||
customer_request = create_customer(user.username, user.email)
|
try:
|
||||||
if customer_request['error'] == None:
|
customer = create_customer(user.username, user.email)
|
||||||
mapping = uncloud_pay.models.StripeCustomer.objects.create(
|
uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create(
|
||||||
owner=user,
|
owner=user, stripe_id=customer.id)
|
||||||
stripe_id=customer_request['response_object']['id']
|
return uncloud_stripe_mapping.stripe_id
|
||||||
)
|
except Exception as e:
|
||||||
return mapping.stripe_id
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@handle_stripe_error
|
@handle_stripe_error
|
||||||
def create_card(customer_id, credit_card):
|
def create_setup_intent(customer_id):
|
||||||
return stripe.Customer.create_source(
|
return stripe.SetupIntent.create(customer=customer_id)
|
||||||
customer_id,
|
|
||||||
card={
|
|
||||||
'number': credit_card.number,
|
|
||||||
'exp_month': credit_card.exp_month,
|
|
||||||
'exp_year': credit_card.exp_year,
|
|
||||||
'cvc': credit_card.cvc
|
|
||||||
})
|
|
||||||
|
|
||||||
@handle_stripe_error
|
@handle_stripe_error
|
||||||
def get_card(customer_id, card_id):
|
def get_setup_intent(setup_intent_id):
|
||||||
return stripe.Customer.retrieve_source(customer_id, card_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
|
@handle_stripe_error
|
||||||
def charge_customer(amount, customer_id, card_id):
|
def charge_customer(amount, customer_id, card_id):
|
||||||
# Amount is in CHF but stripes requires smallest possible unit.
|
# 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)
|
adjusted_amount = int(amount * 100)
|
||||||
return stripe.Charge.create(
|
return stripe.PaymentIntent.create(
|
||||||
amount=adjusted_amount,
|
amount=adjusted_amount,
|
||||||
currency=CURRENCY,
|
currency=CURRENCY,
|
||||||
customer=customer_id,
|
customer=customer_id,
|
||||||
source=card_id)
|
payment_method=card_id,
|
||||||
|
off_session=True,
|
||||||
|
confirm=True,
|
||||||
|
)
|
||||||
|
|
||||||
@handle_stripe_error
|
@handle_stripe_error
|
||||||
def create_customer(name, email):
|
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.shortcuts import render
|
||||||
from django.db import transaction
|
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, views
|
||||||
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
from rest_framework.decorators import renderer_classes
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -13,26 +16,7 @@ from datetime import datetime
|
||||||
import uncloud_pay.stripe as uncloud_stripe
|
import uncloud_pay.stripe as uncloud_stripe
|
||||||
|
|
||||||
###
|
###
|
||||||
# Standard user views:
|
# Payments and Payment Methods.
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
|
@ -48,19 +32,19 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Order.objects.filter(owner=self.request.user)
|
return Order.objects.filter(owner=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodViewSet(viewsets.ModelViewSet):
|
class PaymentMethodViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'create':
|
if self.action == 'create':
|
||||||
return CreatePaymentMethodSerializer
|
return CreatePaymentMethodSerializer
|
||||||
|
elif self.action == 'update':
|
||||||
|
return UpdatePaymentMethodSerializer
|
||||||
elif self.action == 'charge':
|
elif self.action == 'charge':
|
||||||
return ChargePaymentMethodSerializer
|
return ChargePaymentMethodSerializer
|
||||||
else:
|
else:
|
||||||
return PaymentMethodSerializer
|
return PaymentMethodSerializer
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return PaymentMethod.objects.filter(owner=self.request.user)
|
return PaymentMethod.objects.filter(owner=self.request.user)
|
||||||
|
|
||||||
|
@ -70,6 +54,11 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
||||||
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)
|
||||||
|
|
||||||
|
# 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.
|
# 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:
|
if customer_id == None:
|
||||||
|
@ -77,21 +66,26 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
||||||
{'error': 'Could not resolve customer stripe ID.'},
|
{'error': 'Could not resolve customer stripe ID.'},
|
||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
# Register card under stripe customer.
|
try:
|
||||||
credit_card = uncloud_stripe.CreditCard(**serializer.validated_data.pop('credit_card'))
|
setup_intent = uncloud_stripe.create_setup_intent(customer_id)
|
||||||
card_request = uncloud_stripe.create_card(customer_id, credit_card)
|
except Exception as e:
|
||||||
if card_request['error']:
|
return Response({'error': str(e)},
|
||||||
return Response({'stripe_error': card_request['error']}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
card_id = card_request['response_object']['id']
|
|
||||||
|
|
||||||
# Save payment method locally.
|
payment_method = PaymentMethod.objects.create(
|
||||||
serializer.validated_data['stripe_card_id'] = card_request['response_object']['id']
|
owner=request.user,
|
||||||
payment_method = PaymentMethod.objects.create(owner=request.user, **serializer.validated_data)
|
stripe_setup_intent_id=setup_intent.id,
|
||||||
|
**serializer.validated_data)
|
||||||
|
|
||||||
# We do not want to return the credit card details sent with the POST
|
# TODO: find a way to use reverse properly:
|
||||||
# request.
|
# https://www.django-rest-framework.org/api-guide/reverse/
|
||||||
output_serializer = PaymentMethodSerializer(payment_method)
|
path = "payment-method/{}/register-stripe-cc".format(
|
||||||
return Response(output_serializer.data)
|
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'])
|
@action(detail=True, methods=['post'])
|
||||||
def charge(self, request, pk=None):
|
def charge(self, request, pk=None):
|
||||||
|
@ -106,8 +100,96 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
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):
|
class AdminPaymentViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
|
|
Loading…
Reference in a new issue