implement credit card listing

This commit is contained in:
Nico Schottelius 2020-12-28 23:35:34 +01:00
parent e2c4a19049
commit e225bf1cc0
12 changed files with 183 additions and 63 deletions

View File

@ -373,6 +373,10 @@ Q vpn-2a0ae5c1200.ungleich.ch
*** 1.1 (cleanup 1)
**** TODO [#C] Unify ValidationError, FieldError - define proper Exception
- What do we use for model errors
**** TODO [#C] Cleanup the results handling in celery
- Remove the results broker?
- Setup app to ignore results?
- Actually use results?
*** 1.0 (initial release)
**** TODO [#C] Initial Generic product support
- Product

View File

@ -7,8 +7,13 @@
Welcome to uncloud, checkout the following locations:
<ul>
<li><a href="/api/">The API</a>
<li><a href="/cc/reg/">The CC registration</a>
<li><a href="{% url 'api-root' %}">The uncloud API</a>
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
(this is required to be done via Javascript so that we never see
your credit card, but it is sent directly to stripe)
You can list your credit card via the API.
</ul>
</div>

View File

@ -72,10 +72,12 @@ router.register(r'v1/user/register', authviews.AccountManagementViewSet, basenam
router.register(r'v2/net/wireguardvpn', netviews.WireGuardVPNViewSet, basename='wireguardvpnnetwork')
router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basename='wireguardvpnnetworksizes')
# Payment related
router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='credit-card')
urlpatterns = [
path(r'api/', include(router.urls)),
path(r'api/', include(router.urls), name='api'),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), # for login to REST API
path('openapi', get_schema_view(
@ -92,8 +94,6 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"),
path('cc/list/', payviews.ListCards.as_view(), name="cc_list"),
path('cc/delete/<payment_method_id>', payviews.DeleteCard.as_view(), name="cc_delete"),
path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"),
]

View File

@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import FieldError, ValidationError
from uncloud_pay.models import Order
from uncloud_pay.models import Order, Product
class WireGuardVPNPool(models.Model):
"""
@ -123,6 +123,19 @@ class WireGuardVPN(models.Model):
def __str__(self):
return f"{self.address} ({self.pool_index})"
def create_product(self):
"""
Ensure we have a product for the WireguardVPN
"""
pass
# Product.objects.get_or_create(
# name="WireGuardVPN",
# description="Wireguard VPN",
# currency=Currency.CHF,
# config=
class WireGuardVPNFreeLeases(models.Model):
"""

View File

@ -4,9 +4,24 @@ from .models import *
from .selectors import *
from .tasks import *
@transaction.atomic
def create_wireguard_vpn(owner, public_key, network_mask):
# Check if the user has a membership.
#------------------------------------
# If yes, user is eligible for API access and 2 VPNs
# If user already has 2 VPNs, we deduct from the credit
# If deduction is higher than the allowed credit, we fail
#
# Check if the user has suitable balance
# Create order
#
return create_wireguard_vpn_tech(owner, public_key, network_mask)
@transaction.atomic
def create_wireguard_vpn_tech(owner, public_key, network_mask):
pool = get_suitable_pools(network_mask)[0]
count = pool.wireguardvpn_set.count()

View File

@ -88,5 +88,5 @@ admin.site.register(Bill, BillAdmin)
admin.site.register(ProductToRecurringPeriod)
admin.site.register(Product, ProductAdmin)
for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer ]:
for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate, StripeCustomer, StripeCreditCard ]:
admin.site.register(m)

View File

@ -1,4 +1,4 @@
# Generated by Django 3.1 on 2020-12-13 10:38
# Generated by Django 3.1 on 2020-12-28 22:19
from django.conf import settings
import django.core.validators
@ -83,6 +83,18 @@ class Migration(migrations.Migration):
('description', models.TextField(blank=True, default='')),
],
),
migrations.CreateModel(
name='StripeCreditCard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('card_name', models.CharField(default='My credit card', max_length=128)),
('card_id', models.CharField(max_length=32)),
('last4', models.CharField(max_length=4)),
('brand', models.CharField(max_length=64)),
('expiry_date', models.DateField()),
('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ProductToRecurringPeriod',
fields=[
@ -140,6 +152,15 @@ class Migration(migrations.Migration):
('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replaced_by', to='uncloud_pay.order')),
],
),
migrations.CreateModel(
name='Membership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('starting_date', models.DateField(blank=True, null=True)),
('ending_date', models.DateField(blank=True, null=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='BillRecord',
fields=[

View File

@ -17,7 +17,6 @@ from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.conf import settings
import uncloud_pay.stripe
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
from uncloud.models import UncloudAddress
@ -92,6 +91,21 @@ class StripeCustomer(models.Model):
def __str__(self):
return self.owner.username
class StripeCreditCard(models.Model):
owner = models.OneToOneField( 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)
def __str__(self):
return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})"
###
# Payments and Payment Methods.
@ -148,14 +162,14 @@ class PaymentMethod(models.Model):
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' 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 stripe_card_last4(self):
# 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):
@ -1261,3 +1275,24 @@ class ProductToRecurringPeriod(models.Model):
def __str__(self):
return f"{self.product} - {self.recurring_period} (default: {self.is_default})"
class Membership(models.Model):
owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE)
starting_date = models.DateField(blank=True, null=True)
ending_date = models.DateField(blank=True, null=True)
@classmethod
def user_has_membership(user, when):
"""
Return true if user has membership at a point of time,
return false if that is not the case
"""
pass
# cls.objects.filter(owner=user,
# starting_date)

View File

@ -6,13 +6,20 @@ from django.utils.translation import gettext_lazy as _
from .models import *
###
# Checked code
# 2020-12 Checked code
class StripeCreditCardSerializer(serializers.ModelSerializer):
class Meta:
model = StripeCreditCard
exclude = ['card_id', "owner" ]
read_only_fields = [ "last4", "brand", "expiry_date" ]
################################################################################
# Unchecked code
###
# Payments and Payment Methods.
@ -21,13 +28,6 @@ class PaymentSerializer(serializers.ModelSerializer):
model = Payment
fields = '__all__'
class PaymentMethodSerializer(serializers.ModelSerializer):
stripe_card_last4 = serializers.IntegerField()
class Meta:
model = PaymentMethod
fields = [ 'source', 'description', 'primary', 'stripe_card_last4', 'active']
class UpdatePaymentMethodSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethod

View File

@ -1,11 +1,13 @@
import stripe
import stripe.error
import logging
import datetime
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.contrib.auth import get_user_model
import uncloud_pay.models
from .models import StripeCustomer, StripeCreditCard
CURRENCY = 'chf'
@ -56,12 +58,12 @@ def public_api_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
return StripeCustomer.objects.get(owner=user).stripe_id
except ObjectDoesNotExist:
# No entry yet - making a new one.
try:
customer = create_customer(user.username, user.email)
uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create(
uncloud_stripe_mapping = StripeCustomer.objects.create(
owner=user, stripe_id=customer.id)
return uncloud_stripe_mapping.stripe_id
except Exception as e:
@ -109,6 +111,7 @@ def get_customer_cards(customer_id):
customer=customer_id,
type="card",
)
print(stripe_cards["data"])
for stripe_card in stripe_cards["data"]:
card = {}
@ -116,8 +119,24 @@ def get_customer_cards(customer_id):
card['last4'] = stripe_card["card"]["last4"]
card['month'] = stripe_card["card"]["exp_month"]
card['year'] = stripe_card["card"]["exp_year"]
card['id'] = stripe_card["card"]["id"]
card['id'] = stripe_card["id"]
cards.append(card)
return cards
def sync_cards_for_user(user):
customer_id = get_customer_id_for(user)
cards = get_customer_cards(customer_id)
for card in cards:
StripeCreditCard.objects.get_or_create(card_id=card['id'],
owner = user,
defaults = {
'last4': card['last4'],
'brand': card['brand'],
'expiry_date': datetime.date(card['year'],
card['month'],
1)
}
)

View File

@ -63,7 +63,8 @@
} else {
// Return to API on success.
document.getElementById("ungleichmessage").innerHTML
= "Registered credit card with Stripe."
= "Registered credit card with
Stripe. <a href="/">Return to the main page.</a>"
}
});
});

View File

@ -31,22 +31,7 @@ import uncloud_pay.stripe as uncloud_stripe
logger = logging.getLogger(__name__)
###
# Payments and Payment Methods.
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PaymentSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Payment.objects.filter(owner=self.request.user)
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Order.objects.filter(owner=self.request.user)
# 2020-12 checked code
class RegisterCard(LoginRequiredMixin, TemplateView):
login_url = '/login/'
@ -65,6 +50,44 @@ class RegisterCard(LoginRequiredMixin, TemplateView):
context['stripe_pk'] = uncloud_stripe.public_api_key
return context
class CreditCardViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = StripeCreditCardSerializer
permission_classes = [permissions.IsAuthenticated]
def list(self, request):
uncloud_stripe.sync_cards_for_user(self.request.user)
return super().list(request)
def get_queryset(self):
return StripeCreditCard.objects.filter(owner=self.request.user)
###
# Payments and Payment Methods.
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PaymentSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Payment.objects.filter(owner=self.request.user)
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Order.objects.filter(owner=self.request.user)
class ListCards(LoginRequiredMixin, TemplateView):
login_url = '/login/'
@ -80,22 +103,6 @@ class ListCards(LoginRequiredMixin, TemplateView):
return context
class DeleteCard(LoginRequiredMixin, TemplateView):
login_url = '/login/'
template_name = "uncloud_pay/delete_stripe_card.html"
def get_context_data(self, **kwargs):
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
cards = uncloud_stripe.get_customer_cards(customer_id)
context = super().get_context_data(**kwargs)
context['cards'] = cards
context['username'] = self.request.user
return context
class PaymentMethodViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]