From 45f84182df7723874180fb891a7fbf554428a7c0 Mon Sep 17 00:00:00 2001 From: "M.Ravi" Date: Sat, 2 Dec 2023 13:44:43 +0530 Subject: [PATCH] Add generic products --- dynamicweb2/settings.py | 2 + dynamicweb2/stripe_utils.py | 4 +- dynamicweb2/urls.py | 8 +- hosting/models.py | 276 +++++++++++ hosting/urls.py | 5 +- hosting/views.py | 909 +++++++++++++++++++++++++++++++++++- 6 files changed, 1193 insertions(+), 11 deletions(-) diff --git a/dynamicweb2/settings.py b/dynamicweb2/settings.py index 51079bb..f1f4614 100644 --- a/dynamicweb2/settings.py +++ b/dynamicweb2/settings.py @@ -168,6 +168,7 @@ LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID')) # Search union over OUsss AUTH_LDAP_START_TLS = bool(os.environ.get('LDAP_USE_TLS', False)) +SEND_EMAIL = False ENTIRE_SEARCH_BASE = env("ENTIRE_SEARCH_BASE") @@ -259,6 +260,7 @@ BOOTSTRAP3 = { DCL_TEXT = env('DCL_TEXT') DCL_SUPPORT_FROM_ADDRESS = env('DCL_SUPPORT_FROM_ADDRESS') +VM_BASE_PRICE = float(env('VM_BASE_PRICE')) # from django.contrib.sites.models import Site # diff --git a/dynamicweb2/stripe_utils.py b/dynamicweb2/stripe_utils.py index 3b3db99..64b0223 100644 --- a/dynamicweb2/stripe_utils.py +++ b/dynamicweb2/stripe_utils.py @@ -499,7 +499,7 @@ class StripeUtils(object): @handleStripeError def get_or_create_tax_id_for_user(self, stripe_customer_id, vat_number, - type="eu_vat", country=""): + vat_type="eu_vat", country=""): tax_ids_list = stripe.Customer.list_tax_ids( stripe_customer_id, limit=100, @@ -520,7 +520,7 @@ class StripeUtils(object): )) tax_id_obj = stripe.Customer.create_tax_id( stripe_customer_id, - type=type, + type=vat_type, value=vat_number, ) return tax_id_obj diff --git a/dynamicweb2/urls.py b/dynamicweb2/urls.py index 7618306..a681735 100644 --- a/dynamicweb2/urls.py +++ b/dynamicweb2/urls.py @@ -16,8 +16,12 @@ Including another URLconf """ from django.contrib import admin from django.urls import re_path, path, include +from hosting.views import PaymentOrderView urlpatterns = [ path('admin/', admin.site.urls), - re_path('hosting/', include('hosting.urls')) -] + re_path('hosting/', include('hosting.urls')), + re_path(r'^product/(?P[\w-]+)/$', + PaymentOrderView.as_view(), + name='show_product'), +] \ No newline at end of file diff --git a/hosting/models.py b/hosting/models.py index a7efa82..28b25be 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -610,3 +610,279 @@ class UserHostingKey(models.Model): def get_anonymous_user_instance(CustomUser): return CustomUser(email='anonymous@ungleich.ch') + +class VATRates(AssignPermissionsMixin, models.Model): + start_date = models.DateField(blank=True, null=True) + stop_date = models.DateField(blank=True, null=True) + territory_codes = models.TextField(blank=True, default='') + currency_code = models.CharField(max_length=10) + rate = models.FloatField() + rate_type = models.TextField(blank=True, default='') + description = models.TextField(blank=True, default='') + + +class StripeTaxRate(AssignPermissionsMixin, models.Model): + tax_rate_id = models.CharField(max_length=100, unique=True) + jurisdiction = models.CharField(max_length=10) + inclusive = models.BooleanField(default=False) + display_name = models.CharField(max_length=100) + percentage = models.FloatField(default=0) + description = models.CharField(max_length=100) + + +class IncompletePaymentIntents(AssignPermissionsMixin, models.Model): + completed_at = models.DateTimeField(null=True) + created_at = models.DateTimeField(auto_now_add=True) + payment_intent_id = models.CharField(max_length=100) + request = models.TextField() + stripe_api_cus_id = models.CharField(max_length=30) + card_details_response = models.TextField() + stripe_subscription_id = models.CharField(max_length=100, null=True) + stripe_charge_id = models.CharField(max_length=100, null=True) + gp_details = models.TextField() + billing_address_data = models.TextField() + + +class IncompleteSubscriptions(AssignPermissionsMixin, models.Model): + created_at = models.DateTimeField(auto_now_add=True) + completed_at = models.DateTimeField(null=True) + subscription_id = models.CharField(max_length=100) + subscription_status = models.CharField(max_length=30) + name = models.CharField(max_length=50) + email = models.EmailField() + request = models.TextField() + stripe_api_cus_id = models.CharField(max_length=30) + card_details_response = models.TextField() + stripe_subscription_obj = models.TextField() + stripe_onetime_charge = models.TextField() + gp_details = models.TextField() + specs = models.TextField() + vm_template_id = models.PositiveIntegerField(default=0) + template = models.TextField() + billing_address_data = models.TextField() + + +class UserCardDetail(AssignPermissionsMixin, models.Model): + permissions = ('view_usercarddetail',) + stripe_customer = models.ForeignKey(StripeCustomer) + last4 = models.CharField(max_length=4) + brand = models.CharField(max_length=128) + card_id = models.CharField(max_length=100, blank=True, default='') + fingerprint = models.CharField(max_length=100) + exp_month = models.IntegerField(null=False) + exp_year = models.IntegerField(null=False) + preferred = models.BooleanField(default=False) + + class Meta: + permissions = ( + ('view_usercarddetail', 'View User Card'), + ) + + @classmethod + def create(cls, stripe_customer=None, last4=None, brand=None, + fingerprint=None, exp_month=None, exp_year=None, card_id=None, + preferred=False): + instance = cls.objects.create( + stripe_customer=stripe_customer, last4=last4, brand=brand, + fingerprint=fingerprint, exp_month=exp_month, exp_year=exp_year, + card_id=card_id, preferred=preferred + ) + instance.assign_permissions(stripe_customer.user) + return instance + + @classmethod + def get_all_cards_list(cls, stripe_customer): + """ + Get all the cards of the given customer as a list + + :param stripe_customer: The StripeCustomer object + :return: A list of all cards; an empty list if the customer object is + None + """ + cards_list = [] + if stripe_customer is None: + return cards_list + user_card_details = UserCardDetail.objects.filter( + stripe_customer_id=stripe_customer.id + ).order_by('-preferred', 'id') + for card in user_card_details: + cards_list.append({ + 'last4': card.last4, 'brand': card.brand, 'id': card.id, + 'exp_year': card.exp_year, + 'exp_month': '{:02d}'.format(card.exp_month), + 'preferred': card.preferred + }) + return cards_list + + @classmethod + def get_or_create_user_card_detail(cls, stripe_customer, card_details): + """ + A method that checks if a UserCardDetail object exists already + based upon the given card_details and creates it for the given + customer if it does not exist. It returns the UserCardDetail object + matching the given card_details if it exists. + + :param stripe_customer: The given StripeCustomer object to whom the + card object should belong to + :param card_details: A dictionary identifying a given card + :return: UserCardDetail object + """ + try: + if ('fingerprint' in card_details and 'exp_month' in card_details + and 'exp_year' in card_details): + card_detail = UserCardDetail.objects.get( + stripe_customer=stripe_customer, + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'] + ) + else: + raise UserCardDetail.DoesNotExist() + except UserCardDetail.DoesNotExist: + preferred = False + if 'preferred' in card_details: + preferred = card_details['preferred'] + card_detail = UserCardDetail.create( + stripe_customer=stripe_customer, + last4=card_details['last4'], + brand=card_details['brand'], + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'], + card_id=card_details['card_id'], + preferred=preferred + ) + return card_detail + + @staticmethod + def set_default_card(stripe_api_cus_id, stripe_source_id): + """ + Sets the given stripe source as the default source for the given + Stripe customer + :param stripe_api_cus_id: Stripe customer id + :param stripe_source_id: The Stripe source id + :return: + """ + stripe_utils = StripeUtils() + cus_response = stripe_utils.get_customer(stripe_api_cus_id) + cu = cus_response['response_object'] + if stripe_source_id.startswith("pm"): + # card is a payment method + cu.invoice_settings.default_payment_method = stripe_source_id + else: + cu.default_source = stripe_source_id + cu.save() + UserCardDetail.save_default_card_local( + stripe_api_cus_id, stripe_source_id + ) + + @staticmethod + def set_default_card_from_stripe(stripe_api_cus_id): + stripe_utils = StripeUtils() + cus_response = stripe_utils.get_customer(stripe_api_cus_id) + cu = cus_response['response_object'] + default_source = cu.default_source + if default_source is not None: + UserCardDetail.save_default_card_local( + stripe_api_cus_id, default_source + ) + + @staticmethod + def save_default_card_local(stripe_api_cus_id, card_id): + stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id) + user_card_detail = UserCardDetail.objects.get( + stripe_customer=stripe_cust, card_id=card_id + ) + for card in stripe_cust.usercarddetail_set.all(): + card.preferred = False + card.save() + user_card_detail.preferred = True + user_card_detail.save() + + @staticmethod + def get_user_card_details(stripe_customer, card_details): + """ + A utility function to check whether a StripeCustomer is already + associated with the card having given details + + :param stripe_customer: + :param card_details: + :return: The UserCardDetails object if it exists, None otherwise + """ + try: + ucd = UserCardDetail.objects.get( + stripe_customer=stripe_customer, + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'] + ) + return ucd + except UserCardDetail.DoesNotExist: + return None + + +class VMPricing(models.Model): + name = models.CharField(max_length=255, unique=True) + vat_inclusive = models.BooleanField(default=True) + vat_percentage = models.DecimalField( + max_digits=7, decimal_places=5, blank=True, default=0 + ) + cores_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + ram_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + ssd_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + hdd_unit_price = models.DecimalField( + max_digits=7, decimal_places=6, default=0 + ) + discount_name = models.CharField(max_length=255, null=True, blank=True) + discount_amount = models.DecimalField( + max_digits=6, decimal_places=2, default=0 + ) + stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True) + + def __str__(self): + display_str = self.name + ' => ' + ' - '.join([ + '{}/Core'.format(self.cores_unit_price.normalize()), + '{}/GB RAM'.format(self.ram_unit_price.normalize()), + '{}/GB SSD'.format(self.ssd_unit_price.normalize()), + '{}/GB HDD'.format(self.hdd_unit_price.normalize()), + '{}% VAT'.format(self.vat_percentage.normalize()) + if not self.vat_inclusive else 'VAT-Incl', + ]) + if self.discount_amount: + display_str = ' - '.join([ + display_str, + '{} {}'.format( + self.discount_amount, + self.discount_name if self.discount_name else 'Discount' + ) + ]) + return display_str + + @classmethod + def get_vm_pricing_by_name(cls, name): + try: + pricing = VMPricing.objects.get(name=name) + except Exception as e: + logger.error( + "Error getting VMPricing with name {name}. " + "Details: {details}. Attempting to return default" + "pricing.".format(name=name, details=str(e)) + ) + pricing = VMPricing.get_default_pricing() + return pricing + + @classmethod + def get_default_pricing(cls): + """ Returns the default pricing or None """ + try: + default_pricing = VMPricing.objects.get(name='default') + except Exception as e: + logger.error(str(e)) + default_pricing = None + return default_pricing diff --git a/hosting/urls.py b/hosting/urls.py index 0b1cc1b..fd9eac6 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -1,7 +1,7 @@ from django.urls import re_path from django.contrib.auth import views as auth_views -from .views import SSHKeyCreateView, AskSSHKeyView +from .views import SSHKeyCreateView, AskSSHKeyView, PaymentOrderView, OrderConfirmationView from .views import ( #PaymentVMView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, @@ -34,4 +34,7 @@ urlpatterns = [ re_path(r'^validate/(?P.*)/$', SignupValidatedView.as_view(), name='validate'), re_path(r'dashboard/?$', DashboardView.as_view(), name='dashboard'), + re_path(r'^payment/?$', PaymentOrderView.as_view(), name='payment'), + re_path(r'^order-confirmation/?$', OrderConfirmationView.as_view(), + name='order_confirmation'), ] \ No newline at end of file diff --git a/hosting/views.py b/hosting/views.py index 79d9c3c..4d46464 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,32 +1,43 @@ +import json import uuid +import logging +import stripe from django.conf import settings +from django.contrib.auth import authenticate, login from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import LogoutView from django.core.files.base import ContentFile -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404, JsonResponse from django.urls import reverse_lazy, reverse from django.utils.decorators import method_decorator from django.utils.http import urlsafe_base64_decode from django.utils.safestring import mark_safe from django.views import View -from django.views.generic import CreateView, TemplateView, FormView -from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, TemplateView, FormView, DetailView +from django.utils.translation import gettext_lazy as _, get_language from django.views.decorators.cache import never_cache, cache_control from django.contrib import messages from django.shortcuts import render from dynamicweb2.ldap_manager import LdapManager +from dynamicweb2.stripe_utils import StripeUtils from hosting.forms import HostingUserLoginForm, HostingUserSignupForm, ResendActivationEmailForm, \ - PasswordResetRequestForm, UserHostingKeyForm + PasswordResetRequestForm, UserHostingKeyForm, BillingAddressForm, BillingAddressFormSignup, ProductPaymentForm, \ + GenericPaymentForm from hosting.mailer import BaseEmail from hosting.mixins import LoginViewMixin, ResendActivationLinkViewMixin, PasswordResetViewMixin, \ PasswordResetConfirmViewMixin -from hosting.models import CustomUser, UserHostingKey +from hosting.models import CustomUser, UserHostingKey, GenericProduct, StripeCustomer, HostingOrder, UserCardDetail, \ + BillingAddress, IncompletePaymentIntents, StripeTaxRate, IncompleteSubscriptions +from hosting.utils import get_vat_rate_for_country, validate_vat_number, get_vm_price_for_given_vat, \ + get_all_public_keys, create_incomplete_intent_request, get_error_response_dict, show_error decorators = [never_cache] +logger = logging.getLogger(__name__) + class LoginView(LoginViewMixin): template_name = "hosting/login.html" @@ -52,7 +63,7 @@ class SignupView(CreateView): this_base_url = "{0}://{1}".format(self.request.scheme, self.request.get_host()) CustomUser.register(name, password, email, - app='dcl', base_url=this_base_url) + app='dcl', base_url=this_base_url, send_email=settings.SEND_EMAIL) return HttpResponseRedirect(reverse_lazy('hosting:signup-validate')) @@ -351,3 +362,889 @@ class CustomLogoutView(LogoutView): next_page = reverse_lazy('hosting:login') +class PaymentOrderView(FormView): + template_name = 'hositng/landing_payment.html' + + def get_form_class(self): + if self.request.user.is_authenticated(): + return BillingAddressForm + else: + return BillingAddressFormSignup + + def get_context_data(self, **kwargs): + context = super(PaymentOrderView, self).get_context_data(**kwargs) + if 'billing_address_data' in self.request.session: + billing_address_data = self.request.session['billing_address_data'] + else: + billing_address_data = {} + + if self.request.user.is_authenticated(): + if billing_address_data: + billing_address_form = BillingAddressForm( + initial=billing_address_data + ) + else: + billing_address_form = BillingAddressForm( + instance=self.request.user.billing_addresses.order_by('-id').first() + ) + user = self.request.user + if hasattr(user, 'stripecustomer'): + stripe_customer = user.stripecustomer + else: + stripe_customer = None + stripe_utils = StripeUtils() + cards_list_request = stripe_utils.get_available_payment_methods( + stripe_customer + ) + cards_list = cards_list_request.get('response_object') + context.update({'cards_list': cards_list}) + else: + billing_address_form = BillingAddressFormSignup( + initial=billing_address_data + ) + + context.update({ + 'stripe_key': settings.STRIPE_API_PUBLIC_KEY, + 'site_url': reverse('hosting:dashboard'), + 'login_form': HostingUserLoginForm(prefix='login_form'), + 'billing_address_form': billing_address_form, + }) + + if ('generic_payment_type' in self.request.session and + self.request.session['generic_payment_type'] == 'generic'): + if 'product_id' in self.request.session: + product = GenericProduct.objects.get( + id=self.request.session['product_id'] + ) + context.update({'generic_payment_form': ProductPaymentForm( + prefix='generic_payment_form', + initial={'product_name': product.product_name, + 'amount': float(product.get_actual_price()), + 'recurring': product.product_is_subscription, + 'description': product.product_description, + }, + product_id=product.id + ), }) + else: + context.update({'generic_payment_form': GenericPaymentForm( + prefix='generic_payment_form', + ), }) + else: + logger.debug(f"VM creation not implemented") + + return context + + @cache_control(no_cache=True, must_revalidate=True, no_store=True) + def get(self, request, *args, **kwargs): + request.session.pop('vat_validation_status') + request.session.pop('card_id') + request.session.pop('token') + request.session.pop('id_payment_method') + logger.debug("Session: %s" % str(request.session)) + for key, value in request.session.items(): + logger.debug("Session: %s %s" % (key, value)) + if (('type' in request.GET and request.GET['type'] == 'generic') + or 'product_slug' in kwargs): + request.session['generic_payment_type'] = 'generic' + if 'generic_payment_details' in request.session: + request.session.pop('generic_payment_details') + request.session.pop('product_id') + if 'product_slug' in kwargs: + logger.debug("Product slug is " + kwargs['product_slug']) + try: + product = GenericProduct.objects.get( + product_slug=kwargs['product_slug'] + ) + except GenericProduct.DoesNotExist as dne: + logger.error( + "Product '{}' does " + "not exist".format(kwargs['product_slug']) + ) + raise Http404() + request.session['product_id'] = product.id + elif 'specs' not in request.session: + return HttpResponseRedirect(reverse('hosting:dashboard')) + return self.render_to_response(self.get_context_data()) + + def post(self, request, *args, **kwargs): + if 'product' in request.POST: + # query for the supplied product + product = None + try: + product = GenericProduct.objects.get( + id=request.POST['generic_payment_form-product_name'] + ) + except GenericProduct.DoesNotExist as dne: + logger.error( + "The requested product '{}' does not exist".format( + request.POST['generic_payment_form-product_name'] + ) + ) + except GenericProduct.MultipleObjectsReturned as mpe: + logger.error( + "There seem to be more than one product with " + "the name {}".format( + request.POST['generic_payment_form-product_name'] + ) + ) + product = GenericProduct.objects.all( + product_name=request. + POST['generic_payment_form-product_name'] + ).first() + if product is None: + return JsonResponse({}) + else: + return JsonResponse({ + 'amount': product.get_actual_price(), + 'isSubscription': product.product_is_subscription + }) + if 'login_form' in request.POST: + login_form = HostingUserLoginForm( + data=request.POST, prefix='login_form' + ) + if login_form.is_valid(): + email = login_form.cleaned_data.get('email') + password = login_form.cleaned_data.get('password') + auth_user = authenticate(email=email, password=password) + if auth_user: + login(self.request, auth_user) + if 'product_slug' in kwargs: + return HttpResponseRedirect( + reverse('show_product', + kwargs={ + 'product_slug': kwargs['product_slug']} + ) + ) + return HttpResponseRedirect( + reverse('hosting:payment') + ) + else: + context = self.get_context_data() + context['login_form'] = login_form + return self.render_to_response(context) + if request.user.is_authenticated(): + address_form = BillingAddressForm( + data=request.POST, + ) + else: + address_form = BillingAddressFormSignup( + data=request.POST, + ) + if address_form.is_valid(): + # Check if we are in a generic payment case and handle the generic + # payment details form before we go on to verify payment + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + if 'product_id' in request.session: + generic_payment_form = ProductPaymentForm( + data=request.POST, prefix='generic_payment_form', + product_id=request.session['product_id'] + ) + else: + generic_payment_form = GenericPaymentForm( + data=request.POST, prefix='generic_payment_form' + ) + if generic_payment_form.is_valid(): + logger.debug("Generic payment form is valid.") + if 'product_id' in request.session: + product = generic_payment_form.product + else: + product = generic_payment_form.cleaned_data.get( + 'product_name' + ) + user_country_vat_rate = get_vat_rate_for_country( + address_form.cleaned_data["country"] + ) + gp_details = { + "product_name": product.product_name, + "vat_rate": 0 if product.exclude_vat_calculations else + user_country_vat_rate * 100, + "vat_amount": 0 if product.exclude_vat_calculations + else round( + float(product.product_price) * + user_country_vat_rate, 2), + "vat_country": address_form.cleaned_data["country"], + "amount_before_vat": round( + float(product.product_price), 2), + "amount": product.get_actual_price( + vat_rate=get_vat_rate_for_country( + address_form.cleaned_data["country"]) + ), + "recurring": generic_payment_form.cleaned_data.get( + 'recurring' + ), + "description": generic_payment_form.cleaned_data.get( + 'description' + ), + "product_id": product.id, + "product_slug": product.product_slug, + "recurring_interval": + product.product_subscription_interval, + "exclude_vat_calculations": product.exclude_vat_calculations + } + request.session["generic_payment_details"] = ( + gp_details + ) + else: + logger.debug("Generic payment form invalid") + context = self.get_context_data() + context['generic_payment_form'] = generic_payment_form + context['billing_address_form'] = address_form + return self.render_to_response(context) + id_payment_method = self.request.POST.get('id_payment_method', + None) + if id_payment_method == 'undefined': + id_payment_method = address_form.cleaned_data.get('card') + request.session["id_payment_method"] = id_payment_method + logger.debug("id_payment_method is %s" % id_payment_method) + if request.user.is_authenticated(): + this_user = { + 'email': request.user.email, + 'name': request.user.name + } + customer = StripeCustomer.get_or_create( + email=this_user.get('email'), + id_payment_method=id_payment_method + ) + else: + user_email = address_form.cleaned_data.get('email') + user_name = address_form.cleaned_data.get('name') + this_user = { + 'email': user_email, + 'name': user_name + } + try: + custom_user = CustomUser.objects.get(email=user_email) + customer = StripeCustomer.objects.filter( + user_id=custom_user.id).first() + if customer is None: + logger.debug( + ("User {email} is already registered with us." + "But, StripeCustomer does not exist for {email}." + "Hence, creating a new StripeCustomer.").format( + email=user_email + ) + ) + customer = StripeCustomer.create_stripe_api_customer( + email=user_email, + id_payment_method=id_payment_method, + customer_name=user_name) + except CustomUser.DoesNotExist: + logger.debug( + ("StripeCustomer does not exist for {email}." + "Hence, creating a new StripeCustomer.").format( + email=user_email + ) + ) + customer = StripeCustomer.create_stripe_api_customer( + email=user_email, + id_payment_method=id_payment_method, + customer_name=user_name) + + billing_address = address_form.save() + request.session["billing_address_id"] = billing_address.id + request.session['billing_address_data'] = address_form.cleaned_data + request.session['user'] = this_user + # Get or create stripe customer + if not customer: + address_form.add_error( + "__all__", "Invalid credit card" + ) + return self.render_to_response( + self.get_context_data( + billing_address_form=address_form + ) + ) + if type(customer) is StripeCustomer: + request.session['customer'] = customer.stripe_id + else: + request.session['customer'] = customer + + vat_number = address_form.cleaned_data.get('vat_number').strip() + if vat_number: + validate_result = validate_vat_number( + stripe_customer_id=request.session['customer'], + billing_address_id=billing_address.id + ) + + if 'error' in validate_result and validate_result['error']: + messages.add_message( + request, messages.ERROR, validate_result["error"], + extra_tags='vat_error' + ) + return HttpResponseRedirect( + reverse('hosting:payment') + '#vat_error' + ) + request.session["vat_validation_status"] = validate_result["status"] + + # For generic payment we take the user directly to confirmation + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + return HttpResponseRedirect( + reverse('hosting:order_confirmation')) + else: + self.request.session['order_confirm_url'] = reverse('hosting:order_confirmation') + return HttpResponseRedirect( + reverse('hosting:add_ssh_key')) + else: + context = self.get_context_data() + context['billing_address_form'] = address_form + return self.render_to_response(context) + + +class OrderConfirmationView(DetailView, FormView): + form_class = UserHostingKeyForm + template_name = "hosting/order_detail.html" + payment_template_name = 'hosting/landing_payment.html' + context_object_name = "order" + model = HostingOrder + + def get_form_kwargs(self): + kwargs = super(OrderConfirmationView, self).get_form_kwargs() + kwargs.update({'request': self.request}) + return kwargs + + @cache_control(no_cache=True, must_revalidate=True, no_store=True) + def get(self, request, *args, **kwargs): + context = {} + # this is amount to be charge/subscribed before VAT and discount + # and expressed in chf. To convert to cents, multiply by 100 + amount_to_charge = 0 + vm_specs = None + if (('specs' not in request.session or 'user' not in request.session) + and 'generic_payment_type' not in request.session): + return HttpResponseRedirect(reverse('hosting:dashboards')) + if 'id_payment_method' in self.request.session: + payment_method = self.request.session['id_payment_method'] + logger.debug("id_payment_method: %s" % payment_method) + stripe_utils = StripeUtils() + card_details = stripe_utils.get_cards_details_from_payment_method( + payment_method + ) + if not card_details.get('response_object'): + return HttpResponseRedirect(reverse('hosting:payment')) + card_details_response = card_details['response_object'] + context['cc_last4'] = card_details_response['last4'] + context['cc_brand'] = card_details_response['brand'] + context['cc_exp_year'] = card_details_response['exp_year'] + context['cc_exp_month'] = '{:02d}'.format( + card_details_response['exp_month']) + context['id_payment_method'] = payment_method + else: + # TODO check when we go through this case (to me, it seems useless) + card_id = self.request.session.get('card_id') + logger.debug("NO id_payment_method, using card: %s" % card_id) + card_detail = UserCardDetail.objects.get(id=card_id) + context['cc_last4'] = card_detail.last4 + context['cc_brand'] = card_detail.brand + context['cc_exp_year'] = card_detail.exp_year + context['cc_exp_month'] = '{:02d}'.format(card_detail.exp_month) + + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + if "vat_validation_status" in request.session and ( + request.session["vat_validation_status"] == "verified" or + request.session["vat_validation_status"] == "not_needed"): + request.session['generic_payment_details']['vat_rate'] = 0 + request.session['generic_payment_details']['vat_amount'] = 0 + request.session['generic_payment_details']['amount'] = ( + request.session['generic_payment_details']['amount_before_vat'] + ) + context.update({ + 'generic_payment_details': + request.session['generic_payment_details'], + }) + amount_to_charge = request.session['generic_payment_details']['amount'] + else: + vm_specs = request.session.get('specs') + user_vat_country = ( + request.session.get('billing_address_data').get("country") + ) + user_country_vat_rate = get_vat_rate_for_country(user_vat_country) + price, vat, vat_percent, discount = get_vm_price_for_given_vat( + cpu=vm_specs['cpu'], + memory=vm_specs['memory'], + ssd_size=vm_specs['disk_size'], + pricing_name=vm_specs['pricing_name'], + vat_rate=user_country_vat_rate * 100 + ) + vm_specs["price"] = price + vm_specs["price_after_discount"] = price - discount["amount"] + amount_to_charge = price + vat_number = request.session.get('billing_address_data').get("vat_number") + billing_address = BillingAddress.objects.get( + id=request.session["billing_address_id"]) + if vat_number: + validate_result = validate_vat_number( + stripe_customer_id=request.session['customer'], + billing_address_id=billing_address.id + ) + if 'error' in validate_result and validate_result['error']: + messages.add_message( + request, messages.ERROR, validate_result["error"], + extra_tags='vat_error' + ) + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#vat_error' + ) + request.session["vat_validation_status"] = validate_result["status"] + + if user_vat_country.lower() == "ch": + vm_specs["vat"] = vat + vm_specs["vat_percent"] = vat_percent + vm_specs["vat_validation_status"] = "ch_vat" + elif ("vat_validation_status" in request.session and + (request.session["vat_validation_status"] == "verified" or + request.session["vat_validation_status"] == "not_needed")): + vm_specs["vat_percent"] = 0 + vm_specs["vat"] = 0 + vm_specs["vat_validation_status"] = request.session["vat_validation_status"] + else: + vm_specs["vat"] = vat + vm_specs["vat_percent"] = vat_percent + vm_specs["vat_validation_status"] = request.session[ + "vat_validation_status"] if "vat_validation_status" in request.session else "" + vm_specs["vat_country"] = user_vat_country + vm_specs["price_with_vat"] = round(price * (1 + vm_specs["vat_percent"] * 0.01), 2) + vm_specs["price_after_discount"] = round(price - discount['amount'], 2) + vm_specs["price_after_discount_with_vat"] = round( + (price - discount['amount']) * (1 + vm_specs["vat_percent"] * 0.01), 2) + discount["amount_with_vat"] = round(vm_specs["price_with_vat"] - vm_specs["price_after_discount_with_vat"], + 2) + vm_specs["total_price"] = vm_specs["price_after_discount_with_vat"] + vm_specs["discount"] = discount + logger.debug(vm_specs) + request.session['specs'] = vm_specs + + context.update({ + 'vm': vm_specs, + 'form': UserHostingKeyForm(request=self.request), + 'keys': get_all_public_keys(self.request.user) + }) + + is_subscription = False + if ('generic_payment_type' not in request.session or + (request.session['generic_payment_details']['recurring'])): + # Obtain PaymentIntent so that we can initiate and charge + # the customer + is_subscription = True + logger.debug("CASE: Subscription") + else: + logger.debug("CASE: One time payment") + stripe_utils = StripeUtils() + payment_intent_response = stripe_utils.get_payment_intent( + int(amount_to_charge * 100), + customer=request.session['customer'] + ) + payment_intent = payment_intent_response.get( + 'response_object') + if not payment_intent: + logger.error("Could not create payment_intent %s" % + str(payment_intent_response)) + else: + logger.debug("payment_intent.client_secret = %s" % + str(payment_intent.client_secret)) + context.update({ + 'payment_intent_secret': payment_intent.client_secret + }) + logger.debug("Request %s" % create_incomplete_intent_request( + self.request)) + logger.debug("%s" % str(payment_intent)) + logger.debug("customer %s" % request.session['customer']) + logger.debug("card_details_response %s" % card_details_response) + logger.debug("request.session[generic_payment_details] %s" % request.session["generic_payment_details"]) + logger.debug("request.session[billing_address_data] %s" % request.session["billing_address_data"]) + IncompletePaymentIntents.objects.create( + request=create_incomplete_intent_request(self.request), + payment_intent_id=payment_intent.id, + stripe_api_cus_id=request.session['customer'], + card_details_response=json.dumps(card_details_response), + stripe_subscription_id=None, + stripe_charge_id=None, + gp_details=json.dumps(request.session["generic_payment_details"]), + billing_address_data=json.dumps(request.session["billing_address_data"]) + ) + logger.debug("IncompletePaymentIntent done") + + context.update({ + 'site_url': reverse('datacenterlight:index'), + 'page_header_text': _('Confirm Order'), + 'billing_address_data': ( + request.session.get('billing_address_data') + ), + #'cms_integration': get_cms_integration('default'), + 'error_msg': get_error_response_dict("Error", request), + 'success_msg': { + 'msg_title': _("Thank you !"), + 'msg_body': _("Your product will be provisioned as soon as " + "we receive the payment."), + 'redirect': reverse('hosting:invoices') if + request.user.is_authenticated() else + reverse('datacenterlight:index') + }, + 'stripe_key': settings.STRIPE_API_PUBLIC_KEY, + 'is_subscription': str(is_subscription).lower() + }) + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + stripe_onetime_charge = None + stripe_customer_obj = None + gp_details = None + specs = None + vm_template_id = 0 + template = None + user = request.session.get('user') + stripe_api_cus_id = request.session.get('customer') + stripe_utils = StripeUtils() + logger.debug("user=%s stripe_api_cus_id=%s" % (user, stripe_api_cus_id)) + card_details_response = None + new_user_hosting_key_id = None + card_id = None + generic_payment_type = None + generic_payment_details = None + stripe_subscription_obj = None + if 'generic_payment_details' in request.session: + generic_payment_details = request.session[ + 'generic_payment_details'] + if 'generic_payment_type' in request.session: + generic_payment_type = request.session['generic_payment_type'] + if 'new_user_hosting_key_id' in self.request.session: + new_user_hosting_key_id = request.session[ + 'new_user_hosting_key_id'] + if 'card_id' in request.session: + card_id = request.session.get('card_id') + req = { + 'scheme': self.request.scheme, + 'host': self.request.get_host(), + 'language': get_language(), + 'new_user_hosting_key_id': new_user_hosting_key_id, + 'card_id': card_id, + 'generic_payment_type': generic_payment_type, + 'generic_payment_details': generic_payment_details, + 'user': user + } + + if 'id_payment_method' in request.session: + card_details = stripe_utils.get_cards_details_from_payment_method( + request.session.get('id_payment_method') + ) + logger.debug( + "card_details=%s" % (card_details)) + if not card_details.get('response_object'): + msg = card_details.get('error') + return show_error(msg, self.request) + card_details_response = card_details['response_object'] + card_details_dict = { + 'last4': card_details_response['last4'], + 'brand': card_details_response['brand'], + 'card_id': card_details_response['card_id'] + } + stripe_customer_obj = StripeCustomer.objects.filter( + stripe_id=stripe_api_cus_id).first() + if stripe_customer_obj: + ucd = UserCardDetail.get_user_card_details( + stripe_customer_obj, card_details_response + ) + if not ucd: + acc_result = stripe_utils.associate_customer_card( + stripe_api_cus_id, request.session['id_payment_method'], + set_as_default=True + ) + if acc_result['response_object'] is None: + msg = _( + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) + ) + return show_error(msg, self.request) + else: + # Associate PaymentMethod with the stripe customer + # and set it as the default source + acc_result = stripe_utils.associate_customer_card( + stripe_api_cus_id, request.session['id_payment_method'], + set_as_default=True + ) + if acc_result['response_object'] is None: + msg = _( + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) + ) + return show_error(msg, self.request) + elif 'card_id' in request.session: + card_id = request.session.get('card_id') + user_card_detail = UserCardDetail.objects.get(id=card_id) + card_details_dict = { + 'last4': user_card_detail.last4, + 'brand': user_card_detail.brand, + 'card_id': user_card_detail.card_id + } + UserCardDetail.set_default_card( + stripe_api_cus_id=stripe_api_cus_id, + stripe_source_id=user_card_detail.card_id + ) + logger.debug("card_details_dict=%s" % card_details_dict) + else: + response = { + 'status': False, + 'redirect': "{url}#{section}".format( + url=reverse('datacenterlight:payment'), + section='payment_error'), + 'msg_title': str(_('Error.')), + 'msg_body': str( + _('There was a payment related error.' + ' On close of this popup, you will be redirected back to' + ' the payment page.')) + } + return JsonResponse(response) + + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + gp_details = self.request.session['generic_payment_details'] + logger.debug("gp_details=%s" % gp_details) + if gp_details['recurring']: + # generic recurring payment + logger.debug("Commencing a generic recurring payment") + if ('generic_payment_type' not in request.session or + (request.session['generic_payment_details']['recurring'])): + recurring_interval = 'month' + logger.debug("'generic_payment_type' not in request.session or" + "(request.session['generic_payment_details']['recurring']") + if 'generic_payment_details' in request.session: + vat_percent = request.session['generic_payment_details']['vat_rate'] + vat_country = request.session['generic_payment_details']['vat_country'] + if 'discount' in request.session['generic_payment_details']: + discount = request.session['generic_payment_details']['discount'] + else: + discount = {'name': '', 'amount': 0, 'coupon_id': ''} + amount_to_be_charged = ( + round( + request.session['generic_payment_details']['amount_before_vat'], + 2 + ) + ) + plan_name = "generic-{0}-{1:.2f}".format( + request.session['generic_payment_details']['product_id'], + amount_to_be_charged + ) + stripe_plan_id = plan_name + recurring_interval = request.session['generic_payment_details']['recurring_interval'] + if recurring_interval == "year": + plan_name = "{}-yearly".format(plan_name) + stripe_plan_id = plan_name + else: + template = request.session.get('template') + specs = request.session.get('specs') + vm_template_id = template.get('id', 1) + + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = specs.get('price') + vat_percent = specs.get('vat_percent') + vat_country = specs.get('vat_country') + discount = specs.get('discount') + plan_name = StripeUtils.get_stripe_plan_name( + cpu=cpu, + memory=memory, + disk_size=disk_size, + price=amount_to_be_charged + ) + stripe_plan_id = StripeUtils.get_stripe_plan_id( + cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl', + price=amount_to_be_charged + ) + logger.debug(specs) + stripe_plan = stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id, + interval=recurring_interval + ) + # Create StripeTaxRate if applicable to the user + logger.debug("vat_percent = %s, vat_country = %s" % + (vat_percent, vat_country) + ) + stripe_tax_rate = None + if vat_percent > 0: + try: + stripe_tax_rate = StripeTaxRate.objects.get( + description="VAT for %s" % vat_country + ) + print("Stripe Tax Rate exists") + except StripeTaxRate.DoesNotExist as dne: + print("StripeTaxRate does not exist") + tax_rate_obj = stripe.TaxRate.create( + display_name="VAT", + description="VAT for %s" % vat_country, + jurisdiction=vat_country, + percentage=vat_percent, + inclusive=False, + ) + stripe_tax_rate = StripeTaxRate.objects.create( + display_name=tax_rate_obj.display_name, + description=tax_rate_obj.description, + jurisdiction=tax_rate_obj.jurisdiction, + percentage=tax_rate_obj.percentage, + inclusive=False, + tax_rate_id=tax_rate_obj.id + ) + logger.debug("Created StripeTaxRate %s" % + stripe_tax_rate.tax_rate_id) + subscription_result = stripe_utils.subscribe_customer_to_plan( + stripe_api_cus_id, + [{"plan": stripe_plan.get('response_object').stripe_plan_id}], + coupon=(discount['stripe_coupon_id'] + if 'name' in discount and + discount['name'] is not None and + 'ipv6' in discount['name'].lower() and + discount['stripe_coupon_id'] + else ""), + tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [], + default_payment_method=request.session['id_payment_method'] + ) + stripe_subscription_obj = subscription_result.get('response_object') + logger.debug(stripe_subscription_obj) + latest_invoice = stripe.Invoice.retrieve( + stripe_subscription_obj.latest_invoice) + subscription_status = '' + if stripe_subscription_obj: + subscription_status = stripe_subscription_obj.status + + # Check if the subscription was approved and is active + if (stripe_subscription_obj is None + or (stripe_subscription_obj.status != 'active' + and stripe_subscription_obj.status != 'incomplete')): + # At this point, we have created a Stripe API card and + # associated it with the customer; but the transaction failed + # due to some reason. So, we would want to dissociate this card + # here. + # ... + msg = subscription_result.get('error') + return show_error(msg, self.request) + elif stripe_subscription_obj.status == 'incomplete': + # Store params so that they can be retrieved later + IncompleteSubscriptions.objects.create( + subscription_id=stripe_subscription_obj.id, + subscription_status=subscription_status, + name=user.get('name'), + email=user.get('email'), + request=json.dumps(req), + stripe_api_cus_id=stripe_api_cus_id, + card_details_response=json.dumps(card_details_response), + stripe_subscription_obj=json.dumps( + stripe_subscription_obj) if stripe_customer_obj else '', + stripe_onetime_charge=json.dumps( + stripe_onetime_charge) if stripe_onetime_charge else '', + gp_details=json.dumps(gp_details) if gp_details else '', + specs=json.dumps(specs) if specs else '', + vm_template_id=vm_template_id if vm_template_id else 0, + template=json.dumps(template) if template else '', + billing_address_data=json.dumps( + request.session.get('billing_address_data') + ) + ) + pi = stripe.PaymentIntent.retrieve( + latest_invoice.payment_intent + ) + # TODO: requires_attention is probably wrong value to compare + if request.user.is_authenticated(): + if 'generic_payment_details' in request.session: + redirect_url = reverse('hosting:invoices') + else: + redirect_url = reverse('hosting:virtual_machines') + else: + redirect_url = reverse('datacenterlight:index') + + if (pi.status == 'requires_attention' or + pi.status == 'requires_source_action'): + logger.debug("Display SCA authentication %s " % pi.status) + context = { + 'sid': stripe_subscription_obj.id, + 'payment_intent_secret': pi.client_secret, + 'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY, + 'showSCA': True, + 'success': { + 'status': True, + 'redirect': redirect_url, + 'msg_title': str(_('Thank you for the order.')), + 'msg_body': str( + _('Your product will be provisioned as soon as' + ' we receive a payment confirmation from ' + 'Stripe. We will send you a confirmation ' + 'email. You can always contact us at ' + 'support@datacenterlight.ch') + ) + }, + 'error': { + 'status': False, + 'redirect': "{url}#{section}".format( + url=(reverse( + 'show_product', + kwargs={'product_slug': + request.session[ + 'generic_payment_details'] + ['product_slug']} + ) if 'generic_payment_details' in request.session else + reverse('datacenterlight:payment') + ), + section='payment_error' + ), + 'msg_title': str(_('Error.')), + 'msg_body': str( + _('There was a payment related error.' + ' On close of this popup, you will be redirected back to' + ' the payment page.') + ) + } + } + return JsonResponse(context) + else: + logger.debug( + "Handle this case when " + "stripe.subscription_status is incomplete but " + "pi.status is neither requires_attention nor " + "requires_source_action") + msg = subscription_result.get('error') + return show_error(msg, self.request) + # the code below is executed for + # a) subscription case + # b) the subscription object is active itself, without requiring + # SCA + # provisioning_response = do_provisioning( + # req, stripe_api_cus_id, + # card_details_response, stripe_subscription_obj, + # stripe_onetime_charge, gp_details, specs, vm_template_id, + # template, request.session.get('billing_address_data'), + # self.request + # ) + + if (provisioning_response and + type(provisioning_response['response']) == JsonResponse): + new_user = provisioning_response.get('user', None) + if new_user: + login(self.request, new_user) + return provisioning_response['response'] + + response = { + 'status': True, + 'redirect': ( + reverse('hosting:virtual_machines') + if request.user.is_authenticated() + else reverse('datacenterlight:index') + ), + 'msg_title': str(_('Thank you for the order.')), + 'msg_body': str( + _('Your VM will be up and running in a few moments.' + ' We will send you a confirmation email as soon as' + ' it is ready.')) + } + + return JsonResponse(response)