diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 21126bd..381bb62 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -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 diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html index b40c3b4..e5a8318 100644 --- a/uncloud/templates/uncloud/index.html +++ b/uncloud/templates/uncloud/index.html @@ -7,8 +7,13 @@ Welcome to uncloud, checkout the following locations: diff --git a/uncloud/urls.py b/uncloud/urls.py index 343a83c..f163136 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -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/', payviews.DeleteCard.as_view(), name="cc_delete"), path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"), ] diff --git a/uncloud_net/models.py b/uncloud_net/models.py index c768c17..9865a08 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -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): """ diff --git a/uncloud_net/services.py b/uncloud_net/services.py index 437601d..9149f01 100644 --- a/uncloud_net/services.py +++ b/uncloud_net/services.py @@ -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() diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index 2c72274..eb82fb7 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -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) diff --git a/uncloud_pay/migrations/0001_initial.py b/uncloud_pay/migrations/0001_initial.py index b1b68c5..e65f3dd 100644 --- a/uncloud_pay/migrations/0001_initial.py +++ b/uncloud_pay/migrations/0001_initial.py @@ -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=[ diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 18e6f85..abf769c 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -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) diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 94f833e..84a23fd 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -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 diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py index a3dcb23..5b3bb00 100644 --- a/uncloud_pay/stripe.py +++ b/uncloud_pay/stripe.py @@ -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) + } + ) diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html index 82aca74..76265fa 100644 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ b/uncloud_pay/templates/uncloud_pay/register_stripe.html @@ -63,7 +63,8 @@ } else { // Return to API on success. document.getElementById("ungleichmessage").innerHTML - = "Registered credit card with Stripe." + = "Registered credit card with + Stripe. Return to the main page." } }); }); diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index 2f4ba8d..246e922 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -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]