forked from uncloud/uncloud
implement credit card listing
This commit is contained in:
parent
e2c4a19049
commit
e225bf1cc0
12 changed files with 183 additions and 63 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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=[
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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>"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
Loading…
Reference in a new issue