forked from uncloud/uncloud
[credit card] implement payment
This commit is contained in:
parent
e225bf1cc0
commit
1b06d8ee03
16 changed files with 290 additions and 64 deletions
|
@ -47,12 +47,12 @@ router.register(r'v1/service/generic', serviceviews.GenericServiceProductViewSet
|
|||
router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress')
|
||||
router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill')
|
||||
router.register(r'v1/my/order', payviews.OrderViewSet, basename='order')
|
||||
router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment')
|
||||
#router.register(r'v1/my/payment', payviews.PaymentViewSet, basename='payment')
|
||||
router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method')
|
||||
|
||||
# admin/staff urls
|
||||
router.register(r'v1/admin/bill', payviews.AdminBillViewSet, basename='admin/bill')
|
||||
router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment')
|
||||
#router.register(r'v1/admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment')
|
||||
router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/order')
|
||||
router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet)
|
||||
router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet)
|
||||
|
@ -74,6 +74,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam
|
|||
|
||||
# Payment related
|
||||
router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card')
|
||||
router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -83,16 +84,13 @@ urlpatterns = [
|
|||
path('openapi', get_schema_view(
|
||||
title="uncloud",
|
||||
description="uncloud API",
|
||||
version="1.0.0"
|
||||
version="2.0.0"
|
||||
), name='openapi-schema'),
|
||||
|
||||
# web/ = stuff to view in the browser
|
||||
# path('web/vpn/create/', netviews.WireGuardVPNCreateView.as_view(), name="vpncreate"),
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
path('login/', authviews.LoginView.as_view(), name="login"),
|
||||
path('logout/', authviews.LogoutView.as_view(), name="logout"),
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"),
|
||||
|
||||
path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"),
|
||||
|
|
|
@ -10,10 +10,8 @@ from django.core.files.temp import NamedTemporaryFile
|
|||
from django.http import FileResponse
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
from uncloud_pay.models import *
|
||||
|
||||
|
||||
class BillRecordInline(admin.TabularInline):
|
||||
model = BillRecord
|
||||
|
||||
|
@ -85,8 +83,17 @@ class BillAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(Bill, BillAdmin)
|
||||
admin.site.register(ProductToRecurringPeriod)
|
||||
admin.site.register(Product, ProductAdmin)
|
||||
|
||||
for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer, StripeCreditCard ]:
|
||||
for m in [
|
||||
BillRecord,
|
||||
BillingAddress,
|
||||
Order,
|
||||
Payment,
|
||||
ProductToRecurringPeriod,
|
||||
RecurringPeriod,
|
||||
StripeCreditCard,
|
||||
StripeCustomer,
|
||||
VATRate,
|
||||
]:
|
||||
admin.site.register(m)
|
||||
|
|
24
uncloud_pay/migrations/0002_auto_20201228_2244.py
Normal file
24
uncloud_pay/migrations/0002_auto_20201228_2244.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 22:44
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
28
uncloud_pay/migrations/0003_auto_20201228_2256.py
Normal file
28
uncloud_pay/migrations/0003_auto_20201228_2256.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 22:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0002_auto_20201228_2244'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32),
|
||||
),
|
||||
]
|
18
uncloud_pay/migrations/0004_stripecreditcard_active.py
Normal file
18
uncloud_pay/migrations/0004_stripecreditcard_active.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 23:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0003_auto_20201228_2256'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stripecreditcard',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
18
uncloud_pay/migrations/0005_auto_20201228_2335.py
Normal file
18
uncloud_pay/migrations/0005_auto_20201228_2335.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 23:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0004_stripecreditcard_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stripecreditcard',
|
||||
name='active',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
21
uncloud_pay/migrations/0006_auto_20201228_2337.py
Normal file
21
uncloud_pay/migrations/0006_auto_20201228_2337.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 23:37
|
||||
|
||||
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', '0005_auto_20201228_2335'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stripecreditcard',
|
||||
name='owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
17
uncloud_pay/migrations/0007_auto_20201228_2338.py
Normal file
17
uncloud_pay/migrations/0007_auto_20201228_2338.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-12-28 23:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0006_auto_20201228_2337'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='stripecreditcard',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('owner',), name='one_active_card_per_user'),
|
||||
),
|
||||
]
|
18
uncloud_pay/migrations/0008_payment_external_reference.py
Normal file
18
uncloud_pay/migrations/0008_payment_external_reference.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-12-29 00:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0007_auto_20201228_2338'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='external_reference',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
),
|
||||
]
|
18
uncloud_pay/migrations/0009_auto_20201229_0037.py
Normal file
18
uncloud_pay/migrations/0009_auto_20201229_0037.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-12-29 00:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0008_payment_external_reference'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, default='', max_length=256, null=True),
|
||||
),
|
||||
]
|
19
uncloud_pay/migrations/0010_auto_20201229_0042.py
Normal file
19
uncloud_pay/migrations/0010_auto_20201229_0042.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.1 on 2020-12-29 00:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0009_auto_20201229_0037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
|
@ -64,8 +64,8 @@ class Currency(models.TextChoices):
|
|||
Possible currencies to be billed
|
||||
"""
|
||||
CHF = 'CHF', _('Swiss Franc')
|
||||
EUR = 'EUR', _('Euro')
|
||||
USD = 'USD', _('US Dollar')
|
||||
# EUR = 'EUR', _('Euro')
|
||||
# USD = 'USD', _('US Dollar')
|
||||
|
||||
|
||||
def get_balance_for_user(user):
|
||||
|
@ -93,28 +93,30 @@ class StripeCustomer(models.Model):
|
|||
|
||||
|
||||
class StripeCreditCard(models.Model):
|
||||
owner = models.OneToOneField( get_user_model(),
|
||||
on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
|
||||
card_name = models.CharField(null=False, max_length=128, default="My credit card")
|
||||
card_id = models.CharField(null=False, max_length=32)
|
||||
last4 = models.CharField(null=False, max_length=4)
|
||||
brand = models.CharField(null=False, max_length=64)
|
||||
expiry_date = models.DateField(null=False)
|
||||
active = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner'],
|
||||
condition=Q(active=True),
|
||||
name='one_active_card_per_user')
|
||||
]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})"
|
||||
|
||||
|
||||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
class Payment(models.Model):
|
||||
owner = models.ForeignKey(get_user_model(),
|
||||
on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
|
||||
amount = models.DecimalField(
|
||||
default=0.0,
|
||||
max_digits=AMOUNT_MAX_DIGITS,
|
||||
decimal_places=AMOUNT_DECIMALS,
|
||||
validators=[MinValueValidator(0)])
|
||||
|
@ -128,21 +130,18 @@ class Payment(models.Model):
|
|||
('unknown', 'Unknown')
|
||||
),
|
||||
default='unknown')
|
||||
timestamp = models.DateTimeField(editable=False, auto_now_add=True)
|
||||
|
||||
# We override save() in order to active products awaiting payment.
|
||||
def save(self, *args, **kwargs):
|
||||
# _state.adding is switched to false after super(...) call.
|
||||
being_created = self._state.adding
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner)
|
||||
super(Payment, self).save(*args, **kwargs) # Save payment in DB.
|
||||
unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner)
|
||||
currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)
|
||||
|
||||
newly_paid_bills = list(
|
||||
set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment))
|
||||
for bill in newly_paid_bills:
|
||||
bill.activate_products()
|
||||
external_reference = models.CharField(max_length=256, default="", null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}"
|
||||
|
||||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
|
|
|
@ -4,17 +4,33 @@ from uncloud_auth.serializers import UserSerializer
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import *
|
||||
import uncloud_pay.stripe as uncloud_stripe
|
||||
|
||||
###
|
||||
# 2020-12 Checked code
|
||||
|
||||
|
||||
class StripeCreditCardSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = StripeCreditCard
|
||||
exclude = ['card_id', "owner" ]
|
||||
exclude = [ "card_id", "owner" ]
|
||||
read_only_fields = [ "last4", "brand", "expiry_date" ]
|
||||
|
||||
class PaymentSerializer(serializers.ModelSerializer):
|
||||
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
|
||||
|
||||
class Meta:
|
||||
model = Payment
|
||||
fields = '__all__'
|
||||
read_only_fields = [ "external_reference", "source", "timestamp" ]
|
||||
|
||||
def validate(self, data):
|
||||
payment_intent = uncloud_stripe.charge_customer(data['owner'],
|
||||
data['amount'])
|
||||
|
||||
data["external_reference"] = payment_intent["id"]
|
||||
data["source"] = "stripe"
|
||||
|
||||
return data
|
||||
|
||||
|
||||
################################################################################
|
||||
|
@ -23,10 +39,6 @@ class StripeCreditCardSerializer(serializers.ModelSerializer):
|
|||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
class PaymentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Payment
|
||||
fields = '__all__'
|
||||
|
||||
class UpdatePaymentMethodSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -3,7 +3,7 @@ import stripe.error
|
|||
import logging
|
||||
import datetime
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
@ -80,20 +80,6 @@ def get_setup_intent(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.
|
||||
# https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount
|
||||
adjusted_amount = int(amount * 100)
|
||||
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):
|
||||
return stripe.Customer.create(name=name, email=email)
|
||||
|
@ -111,7 +97,6 @@ def get_customer_cards(customer_id):
|
|||
customer=customer_id,
|
||||
type="card",
|
||||
)
|
||||
print(stripe_cards["data"])
|
||||
|
||||
for stripe_card in stripe_cards["data"]:
|
||||
card = {}
|
||||
|
@ -129,7 +114,21 @@ def sync_cards_for_user(user):
|
|||
customer_id = get_customer_id_for(user)
|
||||
cards = get_customer_cards(customer_id)
|
||||
|
||||
active_cards = StripeCreditCard.objects.filter(owner=user,
|
||||
active=True)
|
||||
|
||||
if len(active_cards) > 0:
|
||||
has_active_card = True
|
||||
else:
|
||||
has_active_card = False
|
||||
|
||||
for card in cards:
|
||||
active = False
|
||||
|
||||
if not has_active_card:
|
||||
active = True
|
||||
has_active_card = True
|
||||
|
||||
StripeCreditCard.objects.get_or_create(card_id=card['id'],
|
||||
owner = user,
|
||||
defaults = {
|
||||
|
@ -137,6 +136,36 @@ def sync_cards_for_user(user):
|
|||
'brand': card['brand'],
|
||||
'expiry_date': datetime.date(card['year'],
|
||||
card['month'],
|
||||
1)
|
||||
1),
|
||||
'active': active
|
||||
}
|
||||
)
|
||||
|
||||
@handle_stripe_error
|
||||
def charge_customer(user, amount, currency='CHF'):
|
||||
# Amount is in CHF but stripes requires smallest possible unit.
|
||||
# https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount
|
||||
# FIXME: might need to be adjusted for other currencies
|
||||
|
||||
if currency == 'CHF':
|
||||
adjusted_amount = int(amount * 100)
|
||||
else:
|
||||
return Exception("Programming error: unsupported currency")
|
||||
|
||||
try:
|
||||
card = StripeCreditCard.objects.get(owner=user,
|
||||
active=True)
|
||||
|
||||
except StripeCreditCard.DoesNotExist:
|
||||
raise ValidationError("No active credit card - cannot create payment")
|
||||
|
||||
customer_id = get_customer_id_for(user)
|
||||
|
||||
return stripe.PaymentIntent.create(
|
||||
amount=adjusted_amount,
|
||||
currency=currency,
|
||||
customer=customer_id,
|
||||
payment_method=card.card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
)
|
||||
|
|
|
@ -63,8 +63,7 @@
|
|||
} else {
|
||||
// Return to API on success.
|
||||
document.getElementById("ungleichmessage").innerHTML
|
||||
= "Registered credit card with
|
||||
Stripe. <a href="/">Return to the main page.</a>"
|
||||
= "Registered credit card with Stripe."
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,17 +68,18 @@ class CreditCardViewSet(mixins.RetrieveModelMixin,
|
|||
return StripeCreditCard.objects.filter(owner=self.request.user)
|
||||
|
||||
|
||||
|
||||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class PaymentViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = PaymentSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Payment.objects.filter(owner=self.request.user)
|
||||
|
||||
|
||||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
|
Loading…
Reference in a new issue