diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6b8710a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.git diff --git a/Changelog b/Changelog index 2c2d73d7..44c4dee4 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,53 @@ +3.2: 2021-02-07 + * 8816: Update order confirmation text to better prepared for payment dispute + * supportticket#22990: Fix: can't add a deleted card +3.1: 2021-01-11 + * 8781: Fix error is setting a default card (MR!746) +3.0: 2021-01-07 + * 8393: Implement SCA for stripe payments (MR!745) + * 8691: Implment check_vm_templates management command (MR!744) +2.14: 2020-12-07 + * 8692: Create a script that fixes django db for the order after celery error (MR!743) +2.13: 2020-12-02 + * 8654: Fix 500 error on invoices list for the user contact+devuanhosting.com@virus.media (MR!742) + * 8593: Escape user's ssh key in xml-rpc call to create VM (MR!741) +2.12.1: 2020-07-21 + * 8307: Introduce "Exclude vat calculations" for Generic Products (MR!740) + * Change DE VAT rate to 16% from 19% (MR!739) +2.12: 2020-06-23 + * 7894: Show one time payment invoices (MR!738) +2.11: 2020-06-11 + * Bugfix: Correct the wrong constant name (caused payment to go thru and showing error and VMs not instantiated) +2.10.8: 2020-06-10 + * #8102: Refactor MAX_TIME_TO_WAIT_FOR_VM_TERMINATE to increase time to poll whether VM has been terminated or not (MR!737) +2.10.7: 2020-05-25 + * Bugfix: Handle VM templates deleted in OpenNebula but VM instances still existing (MR!736) + Notes for deployment: + When deploying define a UPDATED_TEMPLATES string represented dictionary value in .env +``` + # Represents Template Ids that were + # deleted and the new template Id to look for the template + # definition + UPDATED_TEMPLATES="{1: 100}" +``` +2.10.6: 2020-03-25 + * Bugfix: Handle Nonetype for discount's name (MR!735) +2.10.5: 2020-03-17 + * Introduce base price for VMs and let admins add stripe_coupon_id (MR!730) + Notes for deployment: + 1. Add env variable `VM_BASE_PRICE` + 2. Migrate datacenterlight app. This introduces the stripe_coupon_code field in the VMPricing. + 3. Create a coupon in stripe with the desired value and note down the stripe's coupon id + 4. Update the discount amount and set the corresponding coupon id in the admin +2.10.3b: 2020-03-05 + * #7773: Use username for communicating with opennebula all the time +2.10.2b: 2020-02-25 + * #7764: Fix uid represented as bytestring + * #7769: [hosting] ssh private key download feature does not work well on Firefox +2.10.1: 2020-02-02: + * Changes the pricing structure of generic products into the pre vat and with vat (like that for VM) + * Shows product name (if exists) in the invoices list if it belongs to a generic product + * Small bugfixes (right alignment of price in the invoice list, show prices with 2 decimal places etc) 2.10: 2020-02-01 * Feature: Introduce new design to show VAT exclusive/VAT inclusive pricing together * Feature: Separate VAT and discount in Stripe diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..50b81cbb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.10.0-alpine3.15 + +WORKDIR /usr/src/app + +RUN apk add --update --no-cache \ + git \ + build-base \ + openldap-dev \ + python3-dev \ + libpq-dev \ + && rm -rf /var/cache/apk/* + +# FIX https://github.com/python-ldap/python-ldap/issues/432 +RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY ./ . diff --git a/Makefile b/Makefile index 67c0c15b..68ff014e 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,12 @@ help: @echo ' make rsync_upload ' @echo ' make install_debian_packages ' +buildimage: + docker build -t dynamicweb:$$(git describe) . + +releaseimage: buildimage + ./release.sh + collectstatic: $(PY?) $(BASEDIR)/manage.py collectstatic diff --git a/datacenterlight/cms_plugins.py b/datacenterlight/cms_plugins.py index c3ec974f..52b4f19f 100644 --- a/datacenterlight/cms_plugins.py +++ b/datacenterlight/cms_plugins.py @@ -1,5 +1,6 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool +from django.conf import settings from .cms_models import ( DCLBannerItemPluginModel, DCLBannerListPluginModel, DCLContactPluginModel, @@ -100,6 +101,7 @@ class DCLCalculatorPlugin(CMSPluginBase): vm_type=instance.vm_type ).order_by('name') context['instance'] = instance + context['vm_base_price'] = settings.VM_BASE_PRICE context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1 return context diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 1a1a2a26..cd7fab99 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-01 09:42+0000\n" +"POT-Creation-Date: 2021-02-07 11:10+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" @@ -144,8 +144,8 @@ msgid "" "the heart of Switzerland." msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz." -msgid "Try now, order a VM. VM price starts from only 10.5 CHF per month." -msgstr "Unser Angebot beginnt bei 10.5 CHF pro Monat. Probier's jetzt aus!" +msgid "Try now, order a VM. VM price starts from only 11.5 CHF per month." +msgstr "Unser Angebot beginnt bei 11.5 CHF pro Monat. Probier's jetzt aus!" msgid "ORDER VM" msgstr "VM BESTELLEN" @@ -415,8 +415,9 @@ msgstr "Deine MwSt-Nummer wurde überprüft" msgid "" "Your VAT number is under validation. VAT will be adjusted, once the " "validation is complete." -msgstr "Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, " -"sobald die Validierung abgeschlossen ist." +msgstr "" +"Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, sobald " +"die Validierung abgeschlossen ist." msgid "Payment method" msgstr "Bezahlmethode" @@ -430,18 +431,6 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" -msgid "Price" -msgstr "Preise" - -msgid "VAT for" -msgstr "MwSt für" - -msgid "Total Amount" -msgstr "Gesamtsumme" - -msgid "Amount" -msgstr "Betrag" - msgid "Description" msgstr "Beschreibung" @@ -454,42 +443,51 @@ msgstr "Preis ohne MwSt." msgid "Pre VAT" msgstr "Exkl. MwSt." +msgid "VAT for" +msgstr "MwSt für" + msgid "Your Price in Total" msgstr "Dein Gesamtpreis" +#, python-format msgid "" -"By clicking \"Place order\" this plan will charge your credit card account " -"with %(total_price)s CHF/year" +" By clicking \"Place order\" you agree to our Terms of Service and " +"this plan will charge your credit card account with %(total_price)s CHF/year" msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s " -"CHF pro Jahr belastet" +"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Jahr belastet." #, python-format msgid "" -"By clicking \"Place order\" this plan will charge your credit card account " -"with %(total_price)s CHF/month" +"\n" +" By clicking \"Place order\" you agree to " +"our Terms " +"of Service and this plan will charge your credit card account with " +"%(total_price)s CHF/month" msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s " -"CHF pro Monat belastet" - -#, fuzzy, python-format -#| msgid "" -#| "By clicking \"Place order\" this payment will charge your credit card " -#| "account with a one time amount of %(total_price)s CHF" -msgid "" -"By clicking \"Place order\" this payment will charge your credit card " -"account with a one time amount of %(total_price)s CHF" -msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " -"%(vm_total_price)s CHF pro Monat belastet" +"\n" +"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Monat belastet." #, python-format msgid "" -"By clicking \"Place order\" this plan will charge your credit card account " -"with %(vm_total_price)s CHF/month" +"By clicking \"Place order\" you agree to our Terms of Service and " +"this plan will charge your credit card account with %(total_price)s CHF" msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " -"%(vm_total_price)s CHF pro Monat belastet" +"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF belastet." + +#, python-format +msgid "" +"By clicking \"Place order\" you agree to our Terms of Service and " +"this plan will charge your credit card account with %(vm_total_price)s CHF/" +"month" +msgstr "" +"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(vm_total_price)s CHF/Monat belastet" msgid "Place order" msgstr "Bestellen" @@ -608,16 +606,22 @@ msgid "Incorrect pricing name. Please contact support{support_email}" msgstr "" "Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}" -#, python-brace-format -msgid "{user} does not have permission to access the card" -msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen" - -msgid "An error occurred. Details: {}" -msgstr "Ein Fehler ist aufgetreten. Details: {}" - msgid "Confirm Order" msgstr "Bestellung Bestätigen" +#, fuzzy +#| msgid "Thank you!" +msgid "Thank you !" +msgstr "Vielen Dank!" + +msgid "Your product will be provisioned as soon as we receive the payment." +msgstr "" + +#, python-brace-format +msgid "An error occurred while associating the card. Details: {details}" +msgstr "" +"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}" + msgid "Error." msgstr "" @@ -628,10 +632,21 @@ msgstr "" "Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom " "Popup zur Bezahlseite weitergeleitet." -#, python-brace-format -msgid "An error occurred while associating the card. Details: {details}" +msgid "Thank you for the order." +msgstr "Danke für Deine Bestellung." + +msgid "" +"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" msgstr "" -"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}" + +msgid "" +"Your VM will be up and running in a few moments. We will send you a " +"confirmation email as soon as it is ready." +msgstr "" +"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " +"auf sie zugreifen kannst." msgid " This is a monthly recurring plan." msgstr "Dies ist ein monatlich wiederkehrender Plan." @@ -671,15 +686,40 @@ msgstr "" "Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst " "jederzeit unter info@ungleich.ch kontaktieren." -msgid "Thank you for the order." -msgstr "Danke für Deine Bestellung." +#, python-format +#~ msgid "" +#~ "By clicking \"Place order\" this plan will charge your credit card " +#~ "account with %(total_price)s CHF/month" +#~ msgstr "" +#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " +#~ "%(total_price)s CHF pro Monat belastet" -msgid "" -"Your VM will be up and running in a few moments. We will send you a " -"confirmation email as soon as it is ready." -msgstr "" -"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " -"auf sie zugreifen kannst." +#, fuzzy, python-format +#~| msgid "" +#~| "By clicking \"Place order\" this payment will charge your credit card " +#~| "account with a one time amount of %(total_price)s CHF" +#~ msgid "" +#~ "By clicking \"Place order\" this payment will charge your credit card " +#~ "account with a one time amount of %(total_price)s CHF" +#~ msgstr "" +#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " +#~ "%(vm_total_price)s CHF pro Monat belastet" + +#, python-brace-format +#~ msgid "{user} does not have permission to access the card" +#~ msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen" + +#~ msgid "An error occurred. Details: {}" +#~ msgstr "Ein Fehler ist aufgetreten. Details: {}" + +#~ msgid "Price" +#~ msgstr "Preise" + +#~ msgid "Total Amount" +#~ msgstr "Gesamtsumme" + +#~ msgid "Amount" +#~ msgstr "Betrag" #~ msgid "Subtotal" #~ msgstr "Zwischensumme" @@ -746,9 +786,6 @@ msgstr "" #~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie " #~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld." -#~ msgid "Thank you!" -#~ msgstr "Vielen Dank!" - #~ msgid "Thank you for order! Our team will contact you via email" #~ msgstr "" #~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich " diff --git a/datacenterlight/management/commands/check_vm_templates.py b/datacenterlight/management/commands/check_vm_templates.py new file mode 100644 index 00000000..db36fde8 --- /dev/null +++ b/datacenterlight/management/commands/check_vm_templates.py @@ -0,0 +1,65 @@ +from django.core.management.base import BaseCommand +from opennebula_api.models import OpenNebulaManager +from datacenterlight.models import VMTemplate +from membership.models import CustomUser + +from django.conf import settings +from time import sleep +import datetime +import json +import logging +import os + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = '''Checks all VM templates to find if they can be instantiated''' + + def add_arguments(self, parser): + parser.add_argument('user_email', type=str) + + def handle(self, *args, **options): + result_dict = {} + user_email = options['user_email'] if 'user_email' in options else "" + + if user_email: + cu = CustomUser.objects.get(email=user_email) + specs = {'cpu': 1, 'memory': 1, 'disk_size': 10} + manager = OpenNebulaManager(email=user_email, password=cu.password) + pub_keys = [settings.TEST_MANAGE_SSH_KEY_PUBKEY] + PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) + if not os.path.exists("%s/outputs" % PROJECT_PATH): + os.mkdir("%s/outputs" % PROJECT_PATH) + for vm_template in VMTemplate.objects.all(): + vm_name = 'test-%s' % vm_template.name + vm_id = manager.create_vm( + template_id=vm_template.opennebula_vm_template_id, + specs=specs, + ssh_key='\n'.join(pub_keys), + vm_name=vm_name + ) + if vm_id and vm_id > 0: + result_dict[vm_name] = "%s OK, created VM %s" % ( + '%s %s %s' % (vm_template.opennebula_vm_template_id, + vm_template.name, vm_template.vm_type), + vm_id + ) + self.stdout.write(self.style.SUCCESS(result_dict[vm_name])) + manager.delete_vm(vm_id) + else: + result_dict[vm_name] = '''Error creating VM %s, template_id + %s %s''' % (vm_name, + vm_template.opennebula_vm_template_id, + vm_template.vm_type) + self.stdout.write(self.style.ERROR(result_dict[vm_name])) + sleep(1) + date_str = datetime.datetime.strftime( + datetime.datetime.now(), '%Y%m%d%H%M%S' + ) + with open("%s/outputs/check_vm_templates_%s.txt" % + (PROJECT_PATH, date_str), + 'w', + encoding='utf-8') as f: + f.write(json.dumps(result_dict)) + self.stdout.write(self.style.SUCCESS("Done")) diff --git a/datacenterlight/management/commands/fix_vm_after_celery_error.py b/datacenterlight/management/commands/fix_vm_after_celery_error.py new file mode 100644 index 00000000..0cfdb423 --- /dev/null +++ b/datacenterlight/management/commands/fix_vm_after_celery_error.py @@ -0,0 +1,76 @@ +from django.core.management.base import BaseCommand +from datacenterlight.tasks import handle_metadata_and_emails +from opennebula_api.models import OpenNebulaManager +from membership.models import CustomUser +import logging +import json + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = '''Updates the DB after manual creation of VM''' + + def add_arguments(self, parser): + parser.add_argument('vm_id', type=int) + parser.add_argument('order_id', type=int) + parser.add_argument('user', type=str) + parser.add_argument('specs', type=str) + parser.add_argument('template', type=str) + + def handle(self, *args, **options): + vm_id = options['vm_id'] + order_id = options['order_id'] + user_str = options['user'] + specs_str = options['specs'] + template_str = options['template'] + + json_acceptable_string = user_str.replace("'", "\"") + user_dict = json.loads(json_acceptable_string) + + json_acceptable_string = specs_str.replace("'", "\"") + specs = json.loads(json_acceptable_string) + + json_acceptable_string = template_str.replace("'", "\"") + template = json.loads(json_acceptable_string) + if vm_id <= 0: + self.stdout.write(self.style.ERROR( + 'vm_id can\'t be less than or 0. Given: %s' % vm_id)) + return + if vm_id <= 0: + self.stdout.write(self.style.ERROR( + 'order_id can\'t be less than or 0. Given: %s' % vm_id)) + return + if specs_str is None or specs_str == "": + self.stdout.write( + self.style.ERROR('specs can\'t be empty or None')) + return + + user = { + 'name': user_dict['name'], + 'email': user_dict['email'], + 'username': user_dict['username'], + 'pass': user_dict['pass'], + 'request_scheme': user_dict['request_scheme'], + 'request_host': user_dict['request_host'], + 'language': user_dict['language'], + } + cu = CustomUser.objects.get(username=user.get('username')) + # Create OpenNebulaManager + self.stdout.write( + self.style.SUCCESS( + 'Connecting using %s' % (cu.username) + ) + ) + manager = OpenNebulaManager(email=cu.username, password=cu.password) + handle_metadata_and_emails(order_id, vm_id, manager, user, specs, + template) + self.stdout.write( + self.style.SUCCESS( + 'Done handling metadata and emails for %s %s %s' % ( + order_id, + vm_id, + str(user) + ) + ) + ) diff --git a/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py b/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py new file mode 100644 index 00000000..d2e45871 --- /dev/null +++ b/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-02-04 03:16 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0030_dclnavbarpluginmodel_show_non_transparent_navbar_always'), + ] + + operations = [ + migrations.AddField( + model_name='vmpricing', + name='stripe_coupon_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index 6410254b..64d785a2 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -54,6 +54,7 @@ class VMPricing(models.Model): 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([ diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 8fea438a..c6869cda 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -225,8 +225,8 @@ } var total = (cardPricing['cpu'].value * window.coresUnitPrice) + (cardPricing['ram'].value * window.ramUnitPrice) + - (cardPricing['storage'].value * window.ssdUnitPrice) - - window.discountAmount; + (cardPricing['storage'].value * window.ssdUnitPrice) + + window.vmBasePrice - window.discountAmount; total = parseFloat(total.toFixed(2)); $("#total").text(total); } diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 55be8099..899b506f 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -56,13 +56,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id): "Running create_vm_task on {}".format(current_task.request.hostname)) vm_id = None try: - final_price = ( - specs.get('total_price') if 'total_price' in specs - else specs.get('price') - ) - if 'pass' in user: - on_user = user.get('email') + on_user = user.get('username') on_pass = user.get('pass') logger.debug("Using user {user} to create VM".format(user=on_user)) vm_name = None @@ -92,107 +87,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id): if vm_id is None: raise Exception("Could not create VM") - # Update HostingOrder with the created vm_id - hosting_order = HostingOrder.objects.filter(id=order_id).first() - error_msg = None - - try: - hosting_order.vm_id = vm_id - hosting_order.save() - logger.debug( - "Updated hosting_order {} with vm_id={}".format( - hosting_order.id, vm_id - ) - ) - except Exception as ex: - error_msg = ( - "HostingOrder with id {order_id} not found. This means that " - "the hosting order was not created and/or it is/was not " - "associated with VM with id {vm_id}. Details {details}".format( - order_id=order_id, vm_id=vm_id, details=str(ex) - ) - ) - logger.error(error_msg) - - stripe_utils = StripeUtils() - result = stripe_utils.set_subscription_metadata( - subscription_id=hosting_order.subscription_id, - metadata={"VM_ID": str(vm_id)} - ) - - if result.get('error') is not None: - emsg = "Could not update subscription metadata for {sub}".format( - sub=hosting_order.subscription_id - ) - logger.error(emsg) - if error_msg: - error_msg += ". " + emsg - else: - error_msg = emsg - - vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data - - context = { - 'name': user.get('name'), - 'email': user.get('email'), - 'cores': specs.get('cpu'), - 'memory': specs.get('memory'), - 'storage': specs.get('disk_size'), - 'price': final_price, - 'template': template.get('name'), - 'vm_name': vm.get('name'), - 'vm_id': vm['vm_id'], - 'order_id': order_id - } - - if error_msg: - context['errors'] = error_msg - if 'pricing_name' in specs: - context['pricing'] = str(VMPricing.get_vm_pricing_by_name( - name=specs['pricing_name'] - )) - email_data = { - 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], - 'body': "\n".join( - ["%s=%s" % (k, v) for (k, v) in context.items()]), - 'reply_to': [context['email']], - } - email = EmailMessage(**email_data) - email.send() - - if 'pass' in user: - lang = 'en-us' - if user.get('language') is not None: - logger.debug( - "Language is set to {}".format(user.get('language'))) - lang = user.get('language') - translation.activate(lang) - # Send notification to the user as soon as VM has been booked - context = { - 'base_url': "{0}://{1}".format(user.get('request_scheme'), - user.get('request_host')), - 'order_url': reverse('hosting:invoices'), - 'page_header': _( - 'Your New VM %(vm_name)s at Data Center Light') % { - 'vm_name': vm.get('name')}, - 'vm_name': vm.get('name') - } - email_data = { - 'subject': context.get('page_header'), - 'to': user.get('email'), - 'context': context, - 'template_name': 'new_booked_vm', - 'template_path': 'hosting/emails/', - 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, - } - email = BaseEmail(**email_data) - email.send() - - logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) - if vm_id > 0: - get_or_create_vm_detail(custom_user, manager, vm_id) + handle_metadata_and_emails(order_id, vm_id, manager, user, specs, + template) except Exception as e: logger.error(str(e)) try: @@ -214,3 +110,127 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id): return return vm_id + + +def handle_metadata_and_emails(order_id, vm_id, manager, user, specs, + template): + """ + Handle's setting up of the metadata in Stripe and database and sending of + emails to the user after VM creation + + :param order_id: the hosting order id + :param vm_id: the id of the vm created + :param manager: the OpenNebula Manager instance + :param user: the user's dict passed to the celery task + :param specs: the specification's dict passed to the celery task + :param template: the template dict passed to the celery task + + :return: + """ + + custom_user = CustomUser.objects.get(email=user.get('email')) + final_price = ( + specs.get('total_price') if 'total_price' in specs + else specs.get('price') + ) + # Update HostingOrder with the created vm_id + hosting_order = HostingOrder.objects.filter(id=order_id).first() + error_msg = None + + try: + hosting_order.vm_id = vm_id + hosting_order.save() + logger.debug( + "Updated hosting_order {} with vm_id={}".format( + hosting_order.id, vm_id + ) + ) + except Exception as ex: + error_msg = ( + "HostingOrder with id {order_id} not found. This means that " + "the hosting order was not created and/or it is/was not " + "associated with VM with id {vm_id}. Details {details}".format( + order_id=order_id, vm_id=vm_id, details=str(ex) + ) + ) + logger.error(error_msg) + + stripe_utils = StripeUtils() + result = stripe_utils.set_subscription_metadata( + subscription_id=hosting_order.subscription_id, + metadata={"VM_ID": str(vm_id)} + ) + + if result.get('error') is not None: + emsg = "Could not update subscription metadata for {sub}".format( + sub=hosting_order.subscription_id + ) + logger.error(emsg) + if error_msg: + error_msg += ". " + emsg + else: + error_msg = emsg + + vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data + + context = { + 'name': user.get('name'), + 'email': user.get('email'), + 'cores': specs.get('cpu'), + 'memory': specs.get('memory'), + 'storage': specs.get('disk_size'), + 'price': final_price, + 'template': template.get('name'), + 'vm_name': vm.get('name'), + 'vm_id': vm['vm_id'], + 'order_id': order_id + } + + if error_msg: + context['errors'] = error_msg + if 'pricing_name' in specs: + context['pricing'] = str(VMPricing.get_vm_pricing_by_name( + name=specs['pricing_name'] + )) + email_data = { + 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['dcl-orders@ungleich.ch'], + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in context.items()]), + 'reply_to': [context['email']], + } + email = EmailMessage(**email_data) + email.send() + + if 'pass' in user: + lang = 'en-us' + if user.get('language') is not None: + logger.debug( + "Language is set to {}".format(user.get('language'))) + lang = user.get('language') + translation.activate(lang) + # Send notification to the user as soon as VM has been booked + context = { + 'base_url': "{0}://{1}".format(user.get('request_scheme'), + user.get('request_host')), + 'order_url': reverse('hosting:invoices'), + 'page_header': _( + 'Your New VM %(vm_name)s at Data Center Light') % { + 'vm_name': vm.get('name')}, + 'vm_name': vm.get('name') + } + email_data = { + 'subject': context.get('page_header'), + 'to': user.get('email'), + 'context': context, + 'template_name': 'new_booked_vm', + 'template_path': 'hosting/emails/', + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, + } + email = BaseEmail(**email_data) + email.send() + + logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) + if vm_id > 0: + get_or_create_vm_detail(custom_user, manager, vm_id) diff --git a/datacenterlight/templates/datacenterlight/cms/calculator.html b/datacenterlight/templates/datacenterlight/cms/calculator.html index 7b123a72..20a6664a 100644 --- a/datacenterlight/templates/datacenterlight/cms/calculator.html +++ b/datacenterlight/templates/datacenterlight/cms/calculator.html @@ -1,5 +1,5 @@
- {% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %} + {% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing vm_base_price=vm_base_price %}
\ No newline at end of file diff --git a/datacenterlight/templates/datacenterlight/emails/welcome_user.html b/datacenterlight/templates/datacenterlight/emails/welcome_user.html index 25185618..2044b2ee 100644 --- a/datacenterlight/templates/datacenterlight/emails/welcome_user.html +++ b/datacenterlight/templates/datacenterlight/emails/welcome_user.html @@ -28,7 +28,7 @@ {% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}

- {% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %} + {% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}

diff --git a/datacenterlight/templates/datacenterlight/emails/welcome_user.txt b/datacenterlight/templates/datacenterlight/emails/welcome_user.txt index 772e51a5..06e8aa33 100644 --- a/datacenterlight/templates/datacenterlight/emails/welcome_user.txt +++ b/datacenterlight/templates/datacenterlight/emails/welcome_user.txt @@ -3,7 +3,7 @@ {% trans "Welcome to Data Center Light!" %} {% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %} -{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %} +{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %} {{ base_url }}{% url 'hosting:create_virtual_machine' %} diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html index f64a9500..2c2b51dd 100644 --- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html +++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html @@ -9,6 +9,7 @@ window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}}; window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}}; window.discountAmount = {{vm_pricing.discount_amount|default:0}}; + window.vmBasePrice = {{vm_base_price|default:0}}; window.minRam = {{min_ram}}; window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}'; diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 02bce0ed..1f7a3cda 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -2,6 +2,14 @@ {% load staticfiles bootstrap3 i18n custom_tags humanize %} {% block content %} +
{% if messages %}
@@ -103,58 +111,61 @@

{% endif %}
-
-
-
-
-

- {% trans "Price Before VAT" %} - {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF -

-
-
-
-
-
-
-
-

-
-
-

{% trans "Pre VAT" %}

-
-
-

{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)

-
+ {% if generic_payment_details.exclude_vat_calculations %} + {% else %} +
+
-
-
-

Price

-
-
-

{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF

-
-
-

{{generic_payment_details.amount|floatformat:2|intcomma}} CHF

-
+
+

+ {% trans "Price Before VAT" %} + {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF +

-
-
-
-
-
-
-
-

Total

-
-
-

{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF

-
-
-

{{generic_payment_details.amount|floatformat:2|intcomma}} CHF

-
+
+
-
+
+
+
+

+
+
+

{% trans "Pre VAT" %}

+
+
+

{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)

+
+
+
+
+

Price

+
+
+

{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF

+
+
+

{{generic_payment_details.amount|floatformat:2|intcomma}} CHF

+
+
+
+
+
+
+
+
+
+

Total

+
+
+

{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF

+
+
+

{{generic_payment_details.amount|floatformat:2|intcomma}} CHF

+
+
+
+ {% endif %}

@@ -267,15 +278,16 @@ {% if generic_payment_details %} {% if generic_payment_details.recurring %} {% if generic_payment_details.recurring_interval == 'year' %} -
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/year{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF/year{% endblocktrans %}.
{% else %} -
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} + By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF/month{% endblocktrans %}.
{% endif %} {% else %} -
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF{% endblocktrans %}.
{% endif %} {% else %} -
{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.
+
{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.
{% endif %}
@@ -318,5 +330,14 @@ {%endblock%} diff --git a/datacenterlight/templatetags/custom_tags.py b/datacenterlight/templatetags/custom_tags.py index 0cb18e5b..120cabbf 100644 --- a/datacenterlight/templatetags/custom_tags.py +++ b/datacenterlight/templatetags/custom_tags.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import resolve, reverse from django.utils.safestring import mark_safe from django.utils.translation import activate, get_language, ugettext_lazy as _ -from hosting.models import GenericProduct +from hosting.models import GenericProduct, HostingOrder from utils.hosting_utils import get_ip_addresses logger = logging.getLogger(__name__) @@ -63,6 +63,41 @@ def escaped_line_break(value): return value.replace("\\n", "\n") +@register.filter('get_line_item_from_hosting_order_charge') +def get_line_item_from_hosting_order_charge(hosting_order_id): + """ + Returns ready-to-use "html" line item to be shown for a charge in the + invoice list page + + :param hosting_order_id: the HostingOrder id + :return: + """ + try: + hosting_order = HostingOrder.objects.get(id = hosting_order_id) + if hosting_order.stripe_charge_id: + return mark_safe(""" + {product_name} + {created_at} + {total} + + {see_invoice_text} + + """.format( + product_name=hosting_order.generic_product.product_name.capitalize(), + created_at=hosting_order.created_at.strftime('%Y-%m-%d'), + total='%.2f' % (hosting_order.price), + receipt_url=reverse('hosting:orders', + kwargs={'pk': hosting_order.id}), + + see_invoice_text=_("See Invoice") + )) + else: + return "" + except Exception as ex: + logger.error("Error %s" % str(ex)) + return "" + + @register.filter('get_line_item_from_stripe_invoice') def get_line_item_from_stripe_invoice(invoice): """ @@ -79,7 +114,7 @@ def get_line_item_from_stripe_invoice(invoice): plan_name = "" for line_data in invoice["lines"]["data"]: if is_first: - plan_name = line_data.plan.name + plan_name = line_data.plan.name if line_data.plan is not None else "" start_date = line_data.period.start end_date = line_data.period.end is_first = False diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index 97bfef4c..4e8094c0 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) eu_countries = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk', 'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it', - 'lv', 'lu', 'mt', 'nl', 'po', 'pt', 'ro','sk', 'si', 'es', + 'lv', 'lu', 'mt', 'nl', 'pl', 'pt', 'ro','sk', 'si', 'es', 'se', 'gb'] @@ -38,6 +38,7 @@ def get_cms_integration(name): def create_vm(billing_address_data, stripe_customer_id, specs, stripe_subscription_obj, card_details_dict, request, vm_template_id, template, user): + logger.debug("In create_vm") billing_address = BillingAddress( cardholder_name=billing_address_data['cardholder_name'], street_address=billing_address_data['street_address'], @@ -102,8 +103,6 @@ def create_vm(billing_address_data, stripe_customer_id, specs, create_vm_task.delay(vm_template_id, user, specs, template, order.id) - clear_all_session_vars(request) - def clear_all_session_vars(request): if request.session is not None: @@ -112,7 +111,8 @@ def clear_all_session_vars(request): 'token', 'customer', 'generic_payment_type', 'generic_payment_details', 'product_id', 'order_confirm_url', 'new_user_hosting_key_id', - 'vat_validation_status', 'billing_address_id']: + 'vat_validation_status', 'billing_address_id', + 'id_payment_method']: if session_var in request.session: del request.session[session_var] diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 185b3e29..5bf68e0a 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,3 +1,4 @@ +import json import logging import stripe @@ -7,7 +8,9 @@ from django.contrib import messages from django.contrib.auth import login, authenticate from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect, JsonResponse, Http404 +from django.http import ( + HttpResponseRedirect, JsonResponse, Http404, HttpResponse +) from django.shortcuts import render from django.utils.translation import get_language, ugettext_lazy as _ from django.views.decorators.cache import cache_control @@ -19,9 +22,8 @@ from hosting.forms import ( ) from hosting.models import ( HostingBill, HostingOrder, UserCardDetail, GenericProduct, UserHostingKey, - StripeTaxRate) + StripeTaxRate, IncompleteSubscriptions, IncompletePaymentIntents) from membership.models import CustomUser, StripeCustomer -from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VMTemplateSerializer from utils.forms import ( BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm, @@ -29,7 +31,7 @@ from utils.forms import ( ) from utils.hosting_utils import ( get_vm_price_with_vat, get_all_public_keys, get_vat_rate_for_country, - get_vm_price_for_given_vat, round_up + get_vm_price_for_given_vat ) from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task @@ -262,9 +264,11 @@ class PaymentOrderView(FormView): stripe_customer = user.stripecustomer else: stripe_customer = None - cards_list = UserCardDetail.get_all_cards_list( - stripe_customer=stripe_customer + 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( @@ -310,6 +314,12 @@ class PaymentOrderView(FormView): @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' @@ -424,8 +434,10 @@ class PaymentOrderView(FormView): ) gp_details = { "product_name": product.product_name, - "vat_rate": user_country_vat_rate * 100, - "vat_amount": round( + "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"], @@ -444,7 +456,8 @@ class PaymentOrderView(FormView): "product_id": product.id, "product_slug": product.product_slug, "recurring_interval": - product.product_subscription_interval + product.product_subscription_interval, + "exclude_vat_calculations": product.exclude_vat_calculations } request.session["generic_payment_details"] = ( gp_details @@ -455,42 +468,20 @@ class PaymentOrderView(FormView): context['generic_payment_form'] = generic_payment_form context['billing_address_form'] = address_form return self.render_to_response(context) - token = address_form.cleaned_data.get('token') - if token is '': - card_id = address_form.cleaned_data.get('card') - try: - user_card_detail = UserCardDetail.objects.get(id=card_id) - if not request.user.has_perm( - 'view_usercarddetail', user_card_detail - ): - raise UserCardDetail.DoesNotExist( - _("{user} does not have permission to access the " - "card").format(user=request.user.email) - ) - except UserCardDetail.DoesNotExist as e: - ex = str(e) - logger.error("Card Id: {card_id}, Exception: {ex}".format( - card_id=card_id, ex=ex - ) - ) - msg = _("An error occurred. Details: {}".format(ex)) - messages.add_message( - self.request, messages.ERROR, msg, - extra_tags='make_charge_error' - ) - return HttpResponseRedirect( - reverse('datacenterlight:payment') + '#payment_error' - ) - request.session['card_id'] = user_card_detail.id - else: - request.session['token'] = token + 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'), token=token + email=this_user.get('email'), + id_payment_method=id_payment_method ) else: user_email = address_form.cleaned_data.get('email') @@ -513,7 +504,7 @@ class PaymentOrderView(FormView): ) customer = StripeCustomer.create_stripe_api_customer( email=user_email, - token=token, + id_payment_method=id_payment_method, customer_name=user_name) except CustomUser.DoesNotExist: logger.debug( @@ -524,7 +515,7 @@ class PaymentOrderView(FormView): ) customer = StripeCustomer.create_stripe_api_customer( email=user_email, - token=token, + id_payment_method=id_payment_method, customer_name=user_name) billing_address = address_form.save() @@ -593,24 +584,33 @@ class OrderConfirmationView(DetailView, FormView): @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('datacenterlight:index')) - if 'token' in self.request.session: - token = self.request.session['token'] + 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_token( - token + card_details = stripe_utils.get_cards_details_from_payment_method( + payment_method ) if not card_details.get('response_object'): - return HttpResponseRedirect(reverse('hosting:payment')) + return HttpResponseRedirect(reverse('datacenterlight: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['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 @@ -624,11 +624,14 @@ class OrderConfirmationView(DetailView, FormView): 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'] + 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 = ( @@ -644,9 +647,10 @@ class OrderConfirmationView(DetailView, FormView): ) 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"]) + 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'], @@ -660,7 +664,6 @@ class OrderConfirmationView(DetailView, FormView): return HttpResponseRedirect( reverse('datacenterlight:payment') + '#vat_error' ) - request.session["vat_validation_status"] = validate_result["status"] if user_vat_country.lower() == "ch": @@ -678,9 +681,9 @@ class OrderConfirmationView(DetailView, FormView): 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_up(price * (1 + vm_specs["vat_percent"] * 0.01), 2) - vm_specs["price_after_discount"] = round_up(price - discount['amount'], 2) - vm_specs["price_after_discount_with_vat"] = round_up((price - discount['amount']) * (1 + vm_specs["vat_percent"] * 0.01), 2) + 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 @@ -692,6 +695,51 @@ class OrderConfirmationView(DetailView, FormView): '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'), @@ -699,42 +747,67 @@ class OrderConfirmationView(DetailView, FormView): 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 'token' in request.session: - card_details = stripe_utils.get_cards_details_from_token( - request.session.get('token') + 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') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - response = { - '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(response) + return show_error(msg, self.request) card_details_response = card_details['response_object'] card_details_dict = { 'last4': card_details_response['last4'], @@ -749,7 +822,7 @@ class OrderConfirmationView(DetailView, FormView): ) if not ucd: acc_result = stripe_utils.associate_customer_card( - stripe_api_cus_id, request.session['token'], + stripe_api_cus_id, request.session['id_payment_method'], set_as_default=True ) if acc_result['response_object'] is None: @@ -759,30 +832,22 @@ class OrderConfirmationView(DetailView, FormView): details=acc_result['error'] ) ) - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - response = { - '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(response) + 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) @@ -791,6 +856,11 @@ class OrderConfirmationView(DetailView, FormView): '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, @@ -808,44 +878,15 @@ class OrderConfirmationView(DetailView, FormView): 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") - else: - # generic one time payment - logger.debug("Commencing a one time payment") - charge_response = stripe_utils.make_charge( - amount=gp_details['amount'], - customer=stripe_api_cus_id - ) - stripe_onetime_charge = charge_response.get('response_object') - - # Check if the payment was approved - if not stripe_onetime_charge: - msg = charge_response.get('error') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - response = { - 'status': False, - 'redirect': "{url}#{section}".format( - url=(reverse('show_product', kwargs={ - 'product_slug': gp_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(response) - 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'] @@ -894,6 +935,7 @@ class OrderConfirmationView(DetailView, FormView): 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, @@ -933,258 +975,138 @@ class OrderConfirmationView(DetailView, FormView): subscription_result = stripe_utils.subscribe_customer_to_plan( stripe_api_cus_id, [{"plan": stripe_plan.get('response_object').stripe_plan_id}], - coupon='ipv6-discount-8chf' if ( - 'name' in discount and - discount['name'] is not None and - 'ipv6' in discount['name'].lower() - ) else "", + 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'): + 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') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - response = { - '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(response) - - # Create user if the user is not logged in and if he is not already - # registered - if not request.user.is_authenticated(): - try: - custom_user = CustomUser.objects.get( - email=user.get('email')) - stripe_customer = StripeCustomer.objects.filter( - user_id=custom_user.id).first() - if stripe_customer is None: - stripe_customer = StripeCustomer.objects.create( - user=custom_user, stripe_id=stripe_api_cus_id - ) - stripe_customer_id = stripe_customer.id - except CustomUser.DoesNotExist: - logger.debug( - "Customer {} does not exist.".format(user.get('email'))) - password = CustomUser.get_random_password() - base_url = "{0}://{1}".format(self.request.scheme, - self.request.get_host()) - custom_user = CustomUser.register( - user.get('name'), password, - user.get('email'), - app='dcl', base_url=base_url, send_email=True, - account_details=password - ) - logger.debug("Created user {}.".format(user.get('email'))) - stripe_customer = StripeCustomer.objects. \ - create(user=custom_user, stripe_id=stripe_api_cus_id) - stripe_customer_id = stripe_customer.id - new_user = authenticate(username=custom_user.email, - password=password) - login(request, new_user) - if 'new_user_hosting_key_id' in self.request.session: - user_hosting_key = UserHostingKey.objects.get(id=self.request.session['new_user_hosting_key_id']) - user_hosting_key.user = new_user - user_hosting_key.save() - - owner = new_user - manager = OpenNebulaManager( - email=owner.email, - password=owner.password - ) - keys_to_save = get_all_public_keys(new_user) - manager.save_key_in_opennebula_user('\n'.join(keys_to_save)) - else: - # We assume that if the user is here, his/her StripeCustomer - # object already exists - stripe_customer_id = request.user.stripecustomer.id - custom_user = request.user - - if 'token' in request.session: - ucd = UserCardDetail.get_or_create_user_card_detail( - stripe_customer=self.request.user.stripecustomer, - card_details=card_details_response - ) - UserCardDetail.save_default_card_local( - self.request.user.stripecustomer.stripe_id, - ucd.card_id - ) - else: - 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 - } - if not user_card_detail.preferred: - UserCardDetail.set_default_card( + 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, - stripe_source_id=user_card_detail.card_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') + ) ) - - # Save billing address - billing_address_data = request.session.get('billing_address_data') - logger.debug('billing_address_data is {}'.format(billing_address_data)) - billing_address_data.update({ - 'user': custom_user.id - }) - - if 'generic_payment_type' in request.session: - stripe_cus = StripeCustomer.objects.filter( - stripe_id=stripe_api_cus_id - ).first() - billing_address = BillingAddress( - cardholder_name=billing_address_data['cardholder_name'], - street_address=billing_address_data['street_address'], - city=billing_address_data['city'], - postal_code=billing_address_data['postal_code'], - country=billing_address_data['country'], - vat_number=billing_address_data['vat_number'] - ) - billing_address.save() - - order = HostingOrder.create( - price=self.request - .session['generic_payment_details']['amount'], - customer=stripe_cus, - billing_address=billing_address, - vm_pricing=VMPricing.get_default_pricing() - ) - - # Create a Hosting Bill - HostingBill.create(customer=stripe_cus, - billing_address=billing_address) - - # Create Billing Address for User if he does not have one - if not stripe_cus.user.billing_addresses.count(): - billing_address_data.update({ - 'user': stripe_cus.user.id - }) - billing_address_user_form = UserBillingAddressForm( - billing_address_data + pi = stripe.PaymentIntent.retrieve( + latest_invoice.payment_intent ) - billing_address_user_form.is_valid() - billing_address_user_form.save() + # 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 self.request.session['generic_payment_details']['recurring']: - # Associate the given stripe subscription with the order - order.set_subscription_id( - stripe_subscription_obj.id, card_details_dict - ) - else: - # Associate the given stripe charge id with the order - order.set_stripe_charge(stripe_onetime_charge) - - # Set order status approved - order.set_approved() - order.generic_payment_description = gp_details["description"] - order.generic_product_id = gp_details["product_id"] - order.save() - # send emails - context = { - 'name': user.get('name'), - 'email': user.get('email'), - 'amount': gp_details['amount'], - 'description': gp_details['description'], - 'recurring': gp_details['recurring'], - 'product_name': gp_details['product_name'], - 'product_id': gp_details['product_id'], - 'order_id': order.id - } - - email_data = { - 'subject': (settings.DCL_TEXT + - " Payment received from %s" % context['email']), - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], - 'body': "\n".join( - ["%s=%s" % (k, v) for (k, v) in context.items()]), - 'reply_to': [context['email']], - } - send_plain_email_task.delay(email_data) - recurring_text = _(" This is a monthly recurring plan.") - if gp_details['recurring_interval'] == "year": - recurring_text = _(" This is an yearly recurring plan.") - - email_data = { - 'subject': _("Confirmation of your payment"), - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': [user.get('email')], - 'body': _("Hi {name},\n\n" - "thank you for your order!\n" - "We have just received a payment of CHF {amount:.2f}" - " from you.{recurring}\n\n" - "Cheers,\nYour Data Center Light team".format( - name=user.get('name'), - amount=gp_details['amount'], - recurring=( - recurring_text - if gp_details['recurring'] else '' - ) - ) - ), - 'reply_to': ['info@ungleich.ch'], - } - send_plain_email_task.delay(email_data) - - response = { - 'status': True, - 'redirect': ( - reverse('hosting:invoices') - if request.user.is_authenticated() - else reverse('datacenterlight:index') - ), - 'msg_title': str(_('Thank you for the payment.')), - 'msg_body': str( - _('You will soon receive a confirmation email of the ' - 'payment. You can always contact us at ' - 'info@ungleich.ch for any question that you may have.') - ) - } - clear_all_session_vars(request) - - return JsonResponse(response) - - user = { - 'name': custom_user.name, - 'email': custom_user.email, - 'pass': custom_user.password, - 'request_scheme': request.scheme, - 'request_host': request.get_host(), - 'language': get_language(), - } - - create_vm( - billing_address_data, stripe_customer_id, specs, - stripe_subscription_obj, card_details_dict, request, - vm_template_id, template, user + 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': ( @@ -1200,3 +1122,488 @@ class OrderConfirmationView(DetailView, FormView): } return JsonResponse(response) + + +def create_incomplete_intent_request(request): + """ + Creates a dictionary of all session variables so that they could be + picked up in the webhook for processing. + + :param request: + :return: + """ + req = { + 'scheme': request.scheme, + 'host': request.get_host(), + 'language': get_language(), + 'new_user_hosting_key_id': request.session.get( + 'new_user_hosting_key_id', None), + 'card_id': request.session.get('card_id', None), + 'generic_payment_type': request.session.get( + 'generic_payment_type', None), + 'generic_payment_details': request.session.get( + 'generic_payment_details', None), + 'user': request.session.get('user', None), + 'id_payment_method': request.session.get('id_payment_method', None), + } + return json.dumps(req) + + +def get_or_create_custom_user(request, stripe_api_cus_id): + new_user = None + name = request.get('user').get('name') + email = request.get('user').get('email') + + try: + custom_user = CustomUser.objects.get(email=email) + stripe_customer = StripeCustomer.objects.filter( + user_id=custom_user.id).first() + if stripe_customer is None: + stripe_customer = StripeCustomer.objects.create( + user=custom_user, stripe_id=stripe_api_cus_id + ) + stripe_customer_id = stripe_customer.id + except CustomUser.DoesNotExist: + logger.debug( + "Customer {} does not exist.".format(email)) + password = CustomUser.get_random_password() + base_url = "{0}://{1}".format(request['scheme'], + request['host']) + custom_user = CustomUser.register( + name, password, + email, + app='dcl', base_url=base_url, send_email=True, + account_details=password + ) + logger.debug("Created user {}.".format(email)) + stripe_customer = StripeCustomer.objects. \ + create(user=custom_user, stripe_id=stripe_api_cus_id) + stripe_customer_id = stripe_customer.id + new_user = authenticate(username=custom_user.email, + password=password) + logger.debug("User %s is authenticated" % custom_user.email) + new_user_hosting_key_id = request.get('new_user_hosting_key_id', None) + if new_user_hosting_key_id: + user_hosting_key = UserHostingKey.objects.get( + id=new_user_hosting_key_id) + user_hosting_key.user = new_user + user_hosting_key.save() + logger.debug("User %s key is saved" % custom_user.email) + return custom_user, new_user + + +def set_user_card(card_id, stripe_api_cus_id, custom_user, + card_details_response): + if card_id: + logger.debug("card_id %s was in request" % 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 + } + #if not user_card_detail.preferred: + UserCardDetail.set_default_card( + stripe_api_cus_id=stripe_api_cus_id, + stripe_source_id=user_card_detail.card_id + ) + else: + logger.debug("card_id was NOT in request, using " + "card_details_response") + ucd = UserCardDetail.get_or_create_user_card_detail( + stripe_customer=custom_user.stripecustomer, + card_details=card_details_response + ) + UserCardDetail.save_default_card_local( + custom_user.stripecustomer.stripe_id, + ucd.card_id + ) + card_details_dict = { + 'last4': ucd.last4, + 'brand': ucd.brand, + 'card_id': ucd.card_id + } + return card_details_dict + + +def do_provisioning_generic( + request, stripe_api_cus_id, card_details_response, + stripe_subscription_id, stripe_charge_id, gp_details, + billing_address_data): + stripe_utils = StripeUtils() + acc_result = stripe_utils.associate_customer_card( + stripe_api_cus_id, request['id_payment_method'], + set_as_default=True + ) + """ + Identical to do_provisioning(), except for the fact that this + is specific to handling provisioning of the generic products + """ + logger.debug("Card %s associate result %s" % ( + request['id_payment_method'], + acc_result.get('response_object') + )) + user = request.get('user', None) + logger.debug("generic_payment_type case") + custom_user, new_user = get_or_create_custom_user( + request, stripe_api_cus_id) + logger.debug("%s %s" % (custom_user.email, custom_user.id)) + + card_id = request.get('card_id', None) + + card_details_dict = set_user_card(card_id, stripe_api_cus_id, custom_user, + card_details_response) + + logger.debug("After card details dict %s" % str(card_details_dict)) + + # Save billing address + billing_address_data.update({ + 'user': custom_user.id + }) + logger.debug('billing_address_data is {}'.format(billing_address_data)) + + stripe_cus = StripeCustomer.objects.filter( + stripe_id=stripe_api_cus_id + ).first() + billing_address = BillingAddress( + cardholder_name=billing_address_data['cardholder_name'], + street_address=billing_address_data['street_address'], + city=billing_address_data['city'], + postal_code=billing_address_data['postal_code'], + country=billing_address_data['country'], + vat_number=billing_address_data['vat_number'] + ) + billing_address.save() + + order = HostingOrder.create( + price=request['generic_payment_details']['amount'], + customer=stripe_cus, + billing_address=billing_address, + vm_pricing=VMPricing.get_default_pricing() + ) + + # Create a Hosting Bill + HostingBill.create(customer=stripe_cus, + billing_address=billing_address) + + # Create Billing Address for User if he does not have one + if not stripe_cus.user.billing_addresses.count(): + billing_address_data.update({ + 'user': stripe_cus.user.id + }) + billing_address_user_form = UserBillingAddressForm( + billing_address_data + ) + billing_address_user_form.is_valid() + billing_address_user_form.save() + + recurring = request['generic_payment_details'].get('recurring') + if recurring: + logger.debug("recurring case") + # Associate the given stripe subscription with the order + order.set_subscription_id( + stripe_subscription_id, card_details_dict + ) + logger.debug("recurring case, set order subscription id done") + else: + logger.debug("one time charge case") + # Associate the given stripe charge id with the order + stripe_onetime_charge = stripe.Charge.retrieve(stripe_charge_id) + order.set_stripe_charge(stripe_onetime_charge) + + # Set order status approved + order.set_approved() + order.generic_payment_description = gp_details["description"] + order.generic_product_id = gp_details["product_id"] + order.save() + logger.debug("Order saved") + # send emails + context = { + 'name': user.get('name'), + 'email': user.get('email'), + 'amount': gp_details['amount'], + 'description': gp_details['description'], + 'recurring': gp_details['recurring'], + 'product_name': gp_details['product_name'], + 'product_id': gp_details['product_id'], + 'order_id': order.id + } + + email_data = { + 'subject': (settings.DCL_TEXT + + " Payment received from %s" % context['email']), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['dcl-orders@ungleich.ch'], + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in context.items()]), + 'reply_to': [context['email']], + } + send_plain_email_task.delay(email_data) + recurring_text = _(" This is a monthly recurring plan.") + if gp_details['recurring_interval'] == "year": + recurring_text = _(" This is an yearly recurring plan.") + + email_data = { + 'subject': _("Confirmation of your payment"), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': [user.get('email')], + 'body': _("Hi {name},\n\n" + "thank you for your order!\n" + "We have just received a payment of CHF {amount:.2f}" + " from you.{recurring}\n\n" + "Cheers,\nYour Data Center Light team".format( + name=user.get('name'), + amount=gp_details['amount'], + recurring=( + recurring_text + if gp_details['recurring'] else '' + ) + ) + ), + 'reply_to': ['info@ungleich.ch'], + } + send_plain_email_task.delay(email_data) + redirect_url = reverse('datacenterlight:index') + logger.debug("Sent user/admin emails") + logger.debug("redirect_url = %s " % redirect_url) + response = { + 'status': True, + 'redirect': redirect_url, + 'msg_title': str(_('Thank you for the payment.')), + 'msg_body': str( + _('You will soon receive a confirmation email of the ' + 'payment. You can always contact us at ' + 'info@ungleich.ch for any question that you may have.') + ) + } + logger.debug("after response") + logger.debug(str(response)) + return HttpResponse(status=200) + + +def do_provisioning(request, stripe_api_cus_id, card_details_response, + stripe_subscription_obj, stripe_onetime_charge, gp_details, + specs, vm_template_id, template, billing_address_data, + real_request): + """ + :param request: a dict + { + 'scheme': 'https', + 'host': 'domain', + 'language': 'en-us', + 'new_user_hosting_key_id': 1, + 'card_id': 1, # if usercarddetail exists already, + 'generic_payment_type': 'generic' # represents a generic payment + 'generic_payment_details': { + 'amount': 100, + 'recurring': + }, + 'user': { + 'name': 'John Doe', + 'email': 'john@doe.com' + } + } + :param stripe_api_cus_id: 'cus_xxxxxxx' the actual stripe customer id str + :param card_details_response: + :param stripe_subscription_obj: The actual Stripe's Subscription Object + :param stripe_onetime_charge: Stripe's Charge object + :param gp_details: + :param specs: + :param vm_template_id: + :param template: + :param real_request: + :return: + """ + + logger.debug("do_provisioning") + user = request.get('user', None) + + # Create user if the user is not logged in and if he is not already + # registered + custom_user, new_user = get_or_create_custom_user( + request, stripe_api_cus_id) + + card_id = request.get('card_id', None) + + card_details_dict = set_user_card(card_id, stripe_api_cus_id, custom_user, + card_details_response) + + # Save billing address + billing_address_data.update({ + 'user': custom_user.id + }) + logger.debug('billing_address_data is {}'.format(billing_address_data)) + + generic_payment_type = request.get('generic_payment_type', None) + if generic_payment_type: + logger.debug("generic_payment_type case") + stripe_cus = StripeCustomer.objects.filter( + stripe_id=stripe_api_cus_id + ).first() + billing_address = BillingAddress( + cardholder_name=billing_address_data['cardholder_name'], + street_address=billing_address_data['street_address'], + city=billing_address_data['city'], + postal_code=billing_address_data['postal_code'], + country=billing_address_data['country'], + vat_number=billing_address_data['vat_number'] + ) + billing_address.save() + + order = HostingOrder.create( + price=request['generic_payment_details']['amount'], + customer=stripe_cus, + billing_address=billing_address, + vm_pricing=VMPricing.get_default_pricing() + ) + + # Create a Hosting Bill + HostingBill.create(customer=stripe_cus, + billing_address=billing_address) + + # Create Billing Address for User if he does not have one + if not stripe_cus.user.billing_addresses.count(): + billing_address_data.update({ + 'user': stripe_cus.user.id + }) + billing_address_user_form = UserBillingAddressForm( + billing_address_data + ) + billing_address_user_form.is_valid() + billing_address_user_form.save() + + recurring = request['generic_payment_details'].get('recurring') + if recurring: + logger.debug("recurring case") + # Associate the given stripe subscription with the order + order.set_subscription_id( + stripe_subscription_obj.id, card_details_dict + ) + logger.debug("recurring case, set order subscription id done") + else: + logger.debug("one time charge case") + # Associate the given stripe charge id with the order + order.set_stripe_charge(stripe_onetime_charge) + + # Set order status approved + order.set_approved() + order.generic_payment_description = gp_details["description"] + order.generic_product_id = gp_details["product_id"] + order.save() + logger.debug("Order saved") + # send emails + context = { + 'name': user.get('name'), + 'email': user.get('email'), + 'amount': gp_details['amount'], + 'description': gp_details['description'], + 'recurring': gp_details['recurring'], + 'product_name': gp_details['product_name'], + 'product_id': gp_details['product_id'], + 'order_id': order.id + } + + email_data = { + 'subject': (settings.DCL_TEXT + + " Payment received from %s" % context['email']), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['dcl-orders@ungleich.ch'], + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in context.items()]), + 'reply_to': [context['email']], + } + send_plain_email_task.delay(email_data) + recurring_text = _(" This is a monthly recurring plan.") + if gp_details['recurring_interval'] == "year": + recurring_text = _(" This is an yearly recurring plan.") + + email_data = { + 'subject': _("Confirmation of your payment"), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': [user.get('email')], + 'body': _("Hi {name},\n\n" + "thank you for your order!\n" + "We have just received a payment of CHF {amount:.2f}" + " from you.{recurring}\n\n" + "Cheers,\nYour Data Center Light team".format( + name=user.get('name'), + amount=gp_details['amount'], + recurring=( + recurring_text + if gp_details['recurring'] else '' + ) + ) + ), + 'reply_to': ['info@ungleich.ch'], + } + send_plain_email_task.delay(email_data) + redirect_url = reverse('datacenterlight:index') + logger.debug("Sent user/admin emails") + if real_request: + clear_all_session_vars(real_request) + if real_request.user.is_authenticated(): + redirect_url = reverse('hosting:invoices') + logger.debug("redirect_url = %s " % redirect_url) + response = { + 'status': True, + 'redirect': redirect_url, + 'msg_title': str(_('Thank you for the payment.')), + 'msg_body': str( + _('You will soon receive a confirmation email of the ' + 'payment. You can always contact us at ' + 'info@ungleich.ch for any question that you may have.') + ) + } + logger.debug("after response") + logger.debug(str(response)) + return {'response': JsonResponse(response), 'user': new_user} + + user = { + 'name': custom_user.name, + 'email': custom_user.email, + 'username': custom_user.username, + 'pass': custom_user.password, + 'request_scheme': request['scheme'], + 'request_host': request['host'], + 'language': request['language'], + } + + create_vm( + billing_address_data, custom_user.stripecustomer.id, specs, + stripe_subscription_obj, card_details_dict, request, + vm_template_id, template, user + ) + + if real_request: + clear_all_session_vars(real_request) + + +def get_error_response_dict(msg, request): + logger.error(msg) + response = { + '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 response + + +def show_error(msg, request): + logger.error(msg) + messages.add_message(request, messages.ERROR, msg, + extra_tags='failed_payment') + return JsonResponse(get_error_response_dict(msg,request)) diff --git a/digitalglarus/locale/de/LC_MESSAGES/django.po b/digitalglarus/locale/de/LC_MESSAGES/django.po index ec96f5dc..ef7a46b5 100644 --- a/digitalglarus/locale/de/LC_MESSAGES/django.po +++ b/digitalglarus/locale/de/LC_MESSAGES/django.po @@ -376,8 +376,6 @@ msgid "" " digitalglarus.ch
\n" " hack4lgarus.ch
\n" " ipv6onlyhosting.com
\n" -" ipv6onlyhosting.ch
\n" -" ipv6onlyhosting.net
\n" " django-hosting.ch
\n" " rails-hosting.ch
\n" " node-hosting.ch
\n" @@ -636,8 +634,8 @@ msgstr "" "Internetangebot der ungleich glarus ag, welches unter den nachfolgenden " "Domains erreichbar ist:

ungleich.ch
datacenterlight.ch
devuanhosting.com
devuanhosting.ch
digitalglarus.ch
hack4lgarus." -"ch
ipv6onlyhosting.com
ipv6onlyhosting.ch
ipv6onlyhosting.net
django-hosting.ch
rails-hosting.ch
node-hosting.ch
blog." +"ch
ipv6onlyhosting.com
django-hosting.ch
rails-hosting.ch" +"
node-hosting.ch
blog." "ungleich.ch

Der Datenschutzbeauftragte des Verantwortlichen ist:

Sanghee Kim
ungleich glarus ag
Bahnhofstrasse 1
8783 " "Linthal (CH)
E-Mail: sanghee." @@ -838,3 +836,4 @@ msgstr "" #~ msgid "index/?$" #~ msgstr "index/?$" + diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index c959c237..62fe2897 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -631,8 +631,6 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = { 'datacenterlight.ch': 'UA-62285904-8', 'devuanhosting.ch': 'UA-62285904-9', 'devuanhosting.com': 'UA-62285904-9', - 'ipv6onlyhosting.ch': 'UA-62285904-10', - 'ipv6onlyhosting.net': 'UA-62285904-10', 'ipv6onlyhosting.com': 'UA-62285904-10', 'comic.ungleich.ch': 'UA-62285904-13', '127.0.0.1:8000': 'localhost', @@ -761,6 +759,15 @@ OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT') FIRST_VM_ID_AFTER_EU_VAT = int_env('FIRST_VM_ID_AFTER_EU_VAT') PRE_EU_VAT_RATE = float(env('PRE_EU_VAT_RATE')) +VM_BASE_PRICE = float(env('VM_BASE_PRICE')) + +UPDATED_TEMPLATES_STR = env('UPDATED_TEMPLATES') +UPDATED_TEMPLATES_DICT = {} +if UPDATED_TEMPLATES_STR: + UPDATED_TEMPLATES_DICT = eval(UPDATED_TEMPLATES_STR) + +MAX_TIME_TO_WAIT_FOR_VM_TERMINATE = int_env( + 'MAX_TIME_TO_WAIT_FOR_VM_TERMINATE', 15) if DEBUG: from .local import * # flake8: noqa diff --git a/dynamicweb/settings/prod.py b/dynamicweb/settings/prod.py index 445748ad..0590ca27 100644 --- a/dynamicweb/settings/prod.py +++ b/dynamicweb/settings/prod.py @@ -28,9 +28,7 @@ ALLOWED_HOSTS = [ ".devuanhosting.ch", ".devuanhosting.com", ".digitalezukunft.ch", - ".ipv6onlyhosting.ch", ".ipv6onlyhosting.com", - ".ipv6onlyhosting.net", ".digitalglarus.ch", ".hack4glarus.ch", ".xn--nglarus-n2a.ch" diff --git a/hosting/forms.py b/hosting/forms.py index 947cee44..8df2bd3e 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -2,6 +2,7 @@ import datetime import logging import subprocess import tempfile +import xml from django import forms from django.conf import settings @@ -207,7 +208,7 @@ class UserHostingKeyForm(forms.ModelForm): logger.debug( "Not a correct ssh format {error}".format(error=str(cpe))) raise forms.ValidationError(KEY_ERROR_MESSAGE) - return openssh_pubkey_str + return xml.sax.saxutils.escape(openssh_pubkey_str) def clean_name(self): INVALID_NAME_MESSAGE = _("Comma not accepted in the name of the key") diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 08bcdd7a..11c4a2a5 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-11-15 16:40+0000\n" +"POT-Creation-Date: 2021-02-07 10:19+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -211,6 +211,9 @@ msgstr "Bezahlbares VM Hosting in der Schweiz" msgid "My Dashboard" msgstr "Mein Dashboard" +msgid "Welcome" +msgstr "" + msgid "My VMs" msgstr "Meine VMs" @@ -364,6 +367,11 @@ msgstr "Abgelehnt" msgid "Billed to" msgstr "Rechnungsadresse" +#, fuzzy +#| msgid "Card Number" +msgid "VAT Number" +msgstr "Kreditkartennummer" + msgid "Payment method" msgstr "Bezahlmethode" @@ -391,6 +399,9 @@ msgstr "Festplattenkapazität" msgid "Subtotal" msgstr "Zwischensumme" +msgid "VAT for" +msgstr "" + msgid "VAT" msgstr "Mehrwertsteuer" @@ -424,18 +435,22 @@ msgstr "ZURÜCK ZUR LISTE" msgid "Some problem encountered. Please try again later." msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." +#, fuzzy +#| msgid "Description" +msgid "Subscriptions" +msgstr "Beschreibung" + +#, fuzzy +#| msgid "One time payment" +msgid "One-time payments" +msgstr "Einmalzahlung" + msgid "VM ID" msgstr "" msgid "IP Address" msgstr "IP-Adresse" -msgid "See Invoice" -msgstr "Siehe Rechnung" - -msgid "Page" -msgstr "Seite" - msgid "Log in" msgstr "Anmelden" @@ -480,11 +495,13 @@ msgstr "Bestellungsübersicht" #, python-format msgid "" -"By clicking \"Place order\" this plan will charge your credit card account " -"with %(vm_price)s CHF/month" +"By clicking \"Place order\" you agree to our Terms of Service and " +"this plan will charge your credit card account with %(vm_price)s CHF/month." msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF " -"pro Monat belastet" +"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren" +" Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(vm_price)s CHF/Monat belastet." msgid "Place order" msgstr "Bestellen" @@ -504,6 +521,12 @@ msgstr "Schliessen" msgid "Order Nr." msgstr "Bestellung Nr." +msgid "See Invoice" +msgstr "Siehe Rechnung" + +msgid "Page" +msgstr "Seite" + msgid "Your Order" msgstr "Deine Bestellung" @@ -572,6 +595,19 @@ msgstr "Absenden" msgid "Password reset" msgstr "Passwort zurücksetzen" +#, fuzzy +#| msgid "Key name" +msgid "My Username" +msgstr "Key-Name" + +msgid "Your VAT number has been verified" +msgstr "" + +msgid "" +"Your VAT number is under validation. VAT will be adjusted, once the " +"validation is complete." +msgstr "" + msgid "UPDATE" msgstr "AKTUALISIEREN" @@ -773,21 +809,15 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden." msgid "The reset password link is no longer valid." msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig." +msgid "Could not set a default card." +msgstr "" + msgid "Card deassociation successful" msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben" -msgid "You are not permitted to do this operation" -msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen" - -msgid "The selected card does not exist" -msgstr "Die ausgewählte Karte existiert nicht" - msgid "Billing address updated successfully" msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert" -msgid "You seem to have already added this card" -msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt" - #, python-brace-format msgid "An error occurred while associating the card. Details: {details}" msgstr "" @@ -852,7 +882,8 @@ msgstr "Ungültige Speicher-Grösse" #, python-brace-format msgid "Incorrect pricing name. Please contact support{support_email}" -msgstr "Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}" +msgstr "" +"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}" msgid "" "We could not find the requested VM. Please " @@ -871,7 +902,9 @@ msgstr "Fehler beenden VM" msgid "" "VM terminate action timed out. Please contact support@datacenterlight.ch for " "further information." -msgstr "VM beendet wegen Zeitüberschreitung. Bitte kontaktiere support@datacenterlight.ch für weitere Informationen." +msgstr "" +"VM beendet wegen Zeitüberschreitung. Bitte kontaktiere " +"support@datacenterlight.ch für weitere Informationen." #, python-format msgid "Virtual Machine %(vm_name)s Cancelled" @@ -882,6 +915,15 @@ msgstr "" "Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es " "noch einmal." +#~ msgid "You are not permitted to do this operation" +#~ msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen" + +#~ msgid "The selected card does not exist" +#~ msgstr "Die ausgewählte Karte existiert nicht" + +#~ msgid "You seem to have already added this card" +#~ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt" + #, python-format #~ msgid "This key exists already with the name \"%(name)s\"" #~ msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits" diff --git a/hosting/migrations/0060_update_DE_vat_covid-19.py b/hosting/migrations/0060_update_DE_vat_covid-19.py new file mode 100644 index 00000000..17c6394b --- /dev/null +++ b/hosting/migrations/0060_update_DE_vat_covid-19.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-06-30 19:12 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0059_stripetaxrate'), + ] + + operations = [ + migrations.RunSQL( + sql=["update hosting_vatrates set stop_date = '2020-06-30' where territory_codes = 'DE' and rate = '0.19'"], + reverse_sql=[ + "update hosting_vatrates set stop_date = null where stop_date = '2020-06-30' and territory_codes = 'DE' and rate = '0.19'"], + ), + migrations.RunSQL( + sql=[ + "insert into hosting_vatrates (start_date, stop_date, territory_codes, currency_code, rate, rate_type, description) values ('2020-07-01',null,'DE', 'EUR', '0.16', 'standard', 'Germany (member state) standard VAT rate - COVID 19 reduced rate')"], + reverse_sql=[ + "delete from hosting_vatrates where description = 'Germany (member state) standard VAT rate - COVID 19 reduced rate' and start_date = '2020-07-01' and territory_codes = 'DE'" ], + ), + + migrations.RunSQL( + sql=[ + "update hosting_stripetaxrate set description = 'VAT for DE pre-COVID-19' where description = 'VAT for DE'"], + reverse_sql=[ + "update hosting_stripetaxrate set description = 'VAT for DE' where description = 'VAT for DE pre-COVID-19'"], + ), + ] diff --git a/hosting/migrations/0061_genericproduct_exclude_vat_calculations.py b/hosting/migrations/0061_genericproduct_exclude_vat_calculations.py new file mode 100644 index 00000000..0bef80d4 --- /dev/null +++ b/hosting/migrations/0061_genericproduct_exclude_vat_calculations.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-07-21 16:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0060_update_DE_vat_covid-19'), + ] + + operations = [ + migrations.AddField( + model_name='genericproduct', + name='exclude_vat_calculations', + field=models.BooleanField(default=False, help_text='When checked VAT calculations are excluded for this product'), + ), + ] diff --git a/hosting/migrations/0062_incompletesubscriptions.py b/hosting/migrations/0062_incompletesubscriptions.py new file mode 100644 index 00000000..0405e086 --- /dev/null +++ b/hosting/migrations/0062_incompletesubscriptions.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-12-23 05:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import utils.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0061_genericproduct_exclude_vat_calculations'), + ] + + operations = [ + migrations.CreateModel( + name='IncompleteSubscriptions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('completed_at', models.DateTimeField()), + ('subscription_id', models.CharField(max_length=100)), + ('subscription_status', models.CharField(max_length=30)), + ('name', models.CharField(max_length=50)), + ('email', models.EmailField(max_length=254)), + ('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()), + ], + bases=(utils.mixins.AssignPermissionsMixin, models.Model), + ), + ] diff --git a/hosting/migrations/0063_auto_20201223_0612.py b/hosting/migrations/0063_auto_20201223_0612.py new file mode 100644 index 00000000..eb4ca9d4 --- /dev/null +++ b/hosting/migrations/0063_auto_20201223_0612.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-12-23 06:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0062_incompletesubscriptions'), + ] + + operations = [ + migrations.AlterField( + model_name='incompletesubscriptions', + name='completed_at', + field=models.DateTimeField(null=True), + ), + ] diff --git a/hosting/migrations/0064_incompletepaymentintents.py b/hosting/migrations/0064_incompletepaymentintents.py new file mode 100644 index 00000000..868e053e --- /dev/null +++ b/hosting/migrations/0064_incompletepaymentintents.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-12-31 10:13 +from __future__ import unicode_literals + +from django.db import migrations, models +import utils.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0063_auto_20201223_0612'), + ] + + operations = [ + migrations.CreateModel( + name='IncompletePaymentIntents', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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.TextField()), + ('stripe_charge_id', models.TextField()), + ('gp_details', models.TextField()), + ('billing_address_data', models.TextField()), + ], + bases=(utils.mixins.AssignPermissionsMixin, models.Model), + ), + ] diff --git a/hosting/migrations/0065_auto_20201231_1041.py b/hosting/migrations/0065_auto_20201231_1041.py new file mode 100644 index 00000000..936ccab1 --- /dev/null +++ b/hosting/migrations/0065_auto_20201231_1041.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2020-12-31 10:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0064_incompletepaymentintents'), + ] + + operations = [ + migrations.AlterField( + model_name='incompletepaymentintents', + name='stripe_charge_id', + field=models.CharField(max_length=100, null=True), + ), + migrations.AlterField( + model_name='incompletepaymentintents', + name='stripe_subscription_id', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 67c55aa2..48238afe 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,4 +1,3 @@ -import decimal import json import logging import os @@ -82,15 +81,22 @@ class GenericProduct(AssignPermissionsMixin, models.Model): product_subscription_interval = models.CharField( max_length=10, default="month", help_text="Choose between `year` and `month`") + exclude_vat_calculations = models.BooleanField( + default=False, + help_text="When checked VAT calculations are excluded for this product" + ) def __str__(self): return self.product_name def get_actual_price(self, vat_rate=None): - VAT = vat_rate if vat_rate is not None else self.product_vat - return round( - float(self.product_price) + float(self.product_price) * float(VAT), 2 - ) + if self.exclude_vat_calculations: + return round(float(self.product_price), 2) + else: + VAT = vat_rate if vat_rate is not None else self.product_vat + return round( + float(self.product_price) + float(self.product_price) * float(VAT), 2 + ) class HostingOrder(AssignPermissionsMixin, models.Model): @@ -163,8 +169,12 @@ class HostingOrder(AssignPermissionsMixin, models.Model): def set_stripe_charge(self, stripe_charge): self.stripe_charge_id = stripe_charge.id - self.last4 = stripe_charge.source.last4 - self.cc_brand = stripe_charge.source.brand + if stripe_charge.source is None: + self.last4 = stripe_charge.payment_method_details.card.last4 + self.cc_brand = stripe_charge.payment_method_details.card.brand + else: + self.last4 = stripe_charge.source.last4 + self.cc_brand = stripe_charge.source.brand self.save() def set_subscription_id(self, subscription_id, cc_details): @@ -666,7 +676,11 @@ class UserCardDetail(AssignPermissionsMixin, models.Model): stripe_utils = StripeUtils() cus_response = stripe_utils.get_customer(stripe_api_cus_id) cu = cus_response['response_object'] - cu.default_source = stripe_source_id + 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 @@ -734,3 +748,35 @@ class StripeTaxRate(AssignPermissionsMixin, models.Model): 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() \ No newline at end of file diff --git a/hosting/static/hosting/js/initial.js b/hosting/static/hosting/js/initial.js index 6b6d744d..36cf6d07 100644 --- a/hosting/static/hosting/js/initial.js +++ b/hosting/static/hosting/js/initial.js @@ -266,8 +266,8 @@ $( document ).ready(function() { } var total = (cardPricing['cpu'].value * window.coresUnitPrice) + (cardPricing['ram'].value * window.ramUnitPrice) + - (cardPricing['storage'].value * window.ssdUnitPrice) - - window.discountAmount; + (cardPricing['storage'].value * window.ssdUnitPrice) + + window.vmBasePrice - window.discountAmount; total = parseFloat(total.toFixed(2)); $("#total").text(total); } diff --git a/hosting/static/hosting/js/payment.js b/hosting/static/hosting/js/payment.js index fa89f218..3c4d67da 100644 --- a/hosting/static/hosting/js/payment.js +++ b/hosting/static/hosting/js/payment.js @@ -84,68 +84,72 @@ $(document).ready(function () { var hasCreditcard = window.hasCreditcard || false; if (!hasCreditcard && window.stripeKey) { var stripe = Stripe(window.stripeKey); - var element_style = { - fonts: [{ - family: 'lato-light', - src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")' - }, { - family: 'lato-regular', - src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")' - } - ], - locale: window.current_lan - }; - var elements = stripe.elements(element_style); - var credit_card_text_style = { - base: { - iconColor: '#666EE8', - color: '#31325F', - lineHeight: '25px', - fontWeight: 300, - fontFamily: "'lato-light', sans-serif", - fontSize: '14px', - '::placeholder': { - color: '#777' + if (window.pm_id) { + + } else { + var element_style = { + fonts: [{ + family: 'lato-light', + src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")' + }, { + family: 'lato-regular', + src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")' } - }, - invalid: { - iconColor: '#eb4d5c', - color: '#eb4d5c', - lineHeight: '25px', - fontWeight: 300, - fontFamily: "'lato-regular', sans-serif", - fontSize: '14px', - '::placeholder': { + ], + locale: window.current_lan + }; + var elements = stripe.elements(element_style); + var credit_card_text_style = { + base: { + iconColor: '#666EE8', + color: '#31325F', + lineHeight: '25px', + fontWeight: 300, + fontFamily: "'lato-light', sans-serif", + fontSize: '14px', + '::placeholder': { + color: '#777' + } + }, + invalid: { + iconColor: '#eb4d5c', color: '#eb4d5c', - fontWeight: 400 + lineHeight: '25px', + fontWeight: 300, + fontFamily: "'lato-regular', sans-serif", + fontSize: '14px', + '::placeholder': { + color: '#eb4d5c', + fontWeight: 400 + } } - } - }; + }; - var enter_ccard_text = "Enter your credit card number"; - if (typeof window.enter_your_card_text !== 'undefined') { - enter_ccard_text = window.enter_your_card_text; + var enter_ccard_text = "Enter your credit card number"; + if (typeof window.enter_your_card_text !== 'undefined') { + enter_ccard_text = window.enter_your_card_text; + } + var cardNumberElement = elements.create('cardNumber', { + style: credit_card_text_style, + placeholder: enter_ccard_text + }); + cardNumberElement.mount('#card-number-element'); + + var cardExpiryElement = elements.create('cardExpiry', { + style: credit_card_text_style + }); + cardExpiryElement.mount('#card-expiry-element'); + + var cardCvcElement = elements.create('cardCvc', { + style: credit_card_text_style + }); + cardCvcElement.mount('#card-cvc-element'); + cardNumberElement.on('change', function (event) { + if (event.brand) { + setBrandIcon(event.brand); + } + }); } - var cardNumberElement = elements.create('cardNumber', { - style: credit_card_text_style, - placeholder: enter_ccard_text - }); - cardNumberElement.mount('#card-number-element'); - - var cardExpiryElement = elements.create('cardExpiry', { - style: credit_card_text_style - }); - cardExpiryElement.mount('#card-expiry-element'); - - var cardCvcElement = elements.create('cardCvc', { - style: credit_card_text_style - }); - cardCvcElement.mount('#card-cvc-element'); - cardNumberElement.on('change', function (event) { - if (event.brand) { - setBrandIcon(event.brand); - } - }); } var submit_form_btn = $('#payment_button_with_creditcard'); @@ -163,7 +167,7 @@ $(document).ready(function () { if (parts.length === 2) return parts.pop().split(";").shift(); } - function submitBillingForm() { + function submitBillingForm(pmId) { var billing_form = $('#billing-form'); var recurring_input = $('#id_generic_payment_form-recurring'); billing_form.append(''); @@ -174,11 +178,40 @@ $(document).ready(function () { billing_form.append(''); } billing_form.append(''); + billing_form.append(''); billing_form.submit(); } var $form_new = $('#payment-form-new'); - $form_new.submit(payWithStripe_new); + $form_new.submit(payWithPaymentIntent); + window.result = ""; + window.card = ""; + function payWithPaymentIntent(e) { + e.preventDefault(); + + function stripePMHandler(paymentMethod) { + // Insert the token ID into the form so it gets submitted to the server + console.log(paymentMethod); + $('#id_payment_method').val(paymentMethod.id); + submitBillingForm(paymentMethod.id); + } + stripe.createPaymentMethod({ + type: 'card', + card: cardNumberElement, + }) + .then(function(result) { + // Handle result.error or result.paymentMethod + window.result = result; + if(result.error) { + var errorElement = document.getElementById('card-errors'); + errorElement.textContent = result.error.message; + } else { + console.log("created paymentMethod " + result.paymentMethod.id); + stripePMHandler(result.paymentMethod); + } + }); + window.card = cardNumberElement; + } function payWithStripe_new(e) { e.preventDefault(); @@ -197,7 +230,7 @@ $(document).ready(function () { } else { var process_text = "Processing"; if (typeof window.processing_text !== 'undefined') { - process_text = window.processing_text + process_text = window.processing_text; } $form_new.find('[type=submit]').html(process_text + ' '); diff --git a/hosting/static/hosting/js/virtual_machine_detail.js b/hosting/static/hosting/js/virtual_machine_detail.js index 28592883..72770182 100644 --- a/hosting/static/hosting/js/virtual_machine_detail.js +++ b/hosting/static/hosting/js/virtual_machine_detail.js @@ -92,50 +92,117 @@ $(document).ready(function() { }); var create_vm_form = $('#virtual_machine_create_form'); - create_vm_form.submit(function () { - $('#btn-create-vm').prop('disabled', true); - $.ajax({ - url: create_vm_form.attr('action'), - type: 'POST', - data: create_vm_form.serialize(), - init: function(){ - ok_btn = $('#createvm-modal-done-btn'); - close_btn = $('#createvm-modal-close-btn'); - ok_btn.addClass('btn btn-success btn-ok btn-wide hide'); - close_btn.addClass('btn btn-danger btn-ok btn-wide hide'); - }, - success: function (data) { - fa_icon = $('.modal-icon > .fa'); - modal_btn = $('#createvm-modal-done-btn'); - $('#createvm-modal-title').text(data.msg_title); - $('#createvm-modal-body').html(data.msg_body); - if (data.redirect) { - modal_btn.attr('href', data.redirect).removeClass('hide'); - } else { - modal_btn.attr('href', ""); - } - if (data.status === true) { - fa_icon.attr('class', 'checkmark'); - } else { - fa_icon.attr('class', 'fa fa-close'); - modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide'); - } - }, - error: function (xmlhttprequest, textstatus, message) { + if (window.isSubscription) { + create_vm_form.submit(function () { + $('#btn-create-vm').prop('disabled', true); + $.ajax({ + url: create_vm_form.attr('action'), + type: 'POST', + data: create_vm_form.serialize(), + init: function () { + ok_btn = $('#createvm-modal-done-btn'); + close_btn = $('#createvm-modal-close-btn'); + ok_btn.addClass('btn btn-success btn-ok btn-wide hide'); + close_btn.addClass('btn btn-danger btn-ok btn-wide hide'); + }, + success: function (data) { + fa_icon = $('.modal-icon > .fa'); + modal_btn = $('#createvm-modal-done-btn'); + if (data.showSCA) { + console.log("Show SCA"); + var stripe = Stripe(data.STRIPE_PUBLISHABLE_KEY); + stripe.confirmCardPayment(data.payment_intent_secret).then(function (result) { + if (result.error) { + // Display error.message in your UI. + modal_btn.attr('href', data.error.redirect).removeClass('hide'); + fa_icon.attr('class', 'fa fa-close'); + modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide'); + $('#createvm-modal-title').text(data.error.msg_title); + $('#createvm-modal-body').html(data.error.msg_body); + } else { + // The payment has succeeded. Display a success message. + modal_btn.attr('href', data.success.redirect).removeClass('hide'); + fa_icon.attr('class', 'checkmark'); + $('#createvm-modal-title').text(data.success.msg_title); + $('#createvm-modal-body').html(data.success.msg_body); + } + }); + $('#3Dsecure-modal').show(); + } else { + $('#createvm-modal-title').text(data.msg_title); + $('#createvm-modal-body').html(data.msg_body); + if (data.redirect) { + modal_btn.attr('href', data.redirect).removeClass('hide'); + } else { + modal_btn.attr('href', ""); + } + if (data.status === true) { + fa_icon.attr('class', 'checkmark'); + } else { + fa_icon.attr('class', 'fa fa-close'); + modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide'); + } + } + }, + error: function (xmlhttprequest, textstatus, message) { fa_icon = $('.modal-icon > .fa'); fa_icon.attr('class', 'fa fa-close'); - if (typeof(create_vm_error_message) !== 'undefined') { + if (typeof (create_vm_error_message) !== 'undefined') { $('#createvm-modal-body').text(create_vm_error_message); } $('#btn-create-vm').prop('disabled', false); $('#createvm-modal-close-btn').removeClass('hide'); - } + } + }); + return false; }); - return false; - }); + } else { + create_vm_form.submit(placeOrderPaymentIntent); + function placeOrderPaymentIntent(e) { + e.preventDefault(); + var stripe = Stripe(window.stripeKey); + stripe.confirmCardPayment( + window.paymentIntentSecret, + { + payment_method: window.pm_id + } + ).then(function(result) { + window.result = result; + fa_icon = $('.modal-icon > .fa'); + modal_btn = $('#createvm-modal-done-btn'); + if (result.error) { + // Display error.message in your UI. + modal_btn.attr('href', error_url).removeClass('hide'); + fa_icon.attr('class', 'fa fa-close'); + modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide'); + $('#createvm-modal-title').text(error_title); + $('#createvm-modal-body').html(result.error.message + " " + error_msg); + } else { + // The payment has succeeded + // Display a success message + modal_btn.attr('href', success_url).removeClass('hide'); + fa_icon.attr('class', 'checkmark'); + $('#createvm-modal-title').text(success_title); + $('#createvm-modal-body').html(success_msg); + } + }); + } + } $('#createvm-modal').on('hidden.bs.modal', function () { $(this).find('.modal-footer .btn').addClass('hide'); - }) + }); + + // Toggle subscription and one-time payments div + $('#li-one-time-charges').click(function() { + console.log("li-one-time-charges clicked"); + $('#subscriptions').hide(); + $('#one-time-charges').show(); + }); + $('#li-subscriptions').click(function() { + console.log("li-subscriptions clicked"); + $('#one-time-charges').hide(); + $('#subscriptions').show(); + }); }); window.onload = function () { diff --git a/hosting/templates/hosting/invoices.html b/hosting/templates/hosting/invoices.html index 1a97fd1f..347b1ff4 100644 --- a/hosting/templates/hosting/invoices.html +++ b/hosting/templates/hosting/invoices.html @@ -15,6 +15,11 @@
+ +
@@ -66,6 +71,60 @@ {% endif %} {% endif %} + + +
+ + + + + + + + + + {% for ho, stripe_charge_data in invs_charge %} + + {{ ho.id | get_line_item_from_hosting_order_charge }} + + {% endfor %} + +
{% trans "Product" %}{% trans "Date" %}{% trans "Amount" %}
+{% if invs_charge.has_other_pages %} +
    + {% if invs_charge.has_previous %} + {% if user_email %} +
  • «
  • + {% else %} +
  • «
  • + {% endif %} + {% else %} +
  • «
  • + {% endif %} + {% for i in invs_charge.paginator.page_range %} + {% if invs_charge.number == i %} +
  • {{ i }} (current)
  • + {% else %} + {% if user_email %} +
  • {{ i }}
  • + {% else %} +
  • {{ i }}
  • + {% endif %} + {% endif %} + {% endfor %} + {% if invs_charge.has_next %} + {% if user_email %} +
  • »
  • + {% else %} +
  • »
  • + {% endif %} + {% else %} +
  • »
  • + {% endif %} +
+{% endif %} +
{% endblock %} diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 9256271a..dee453d5 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -218,7 +218,7 @@ {% csrf_token %}
-
{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.
+
{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ vm_price }} CHF/month.{% endblocktrans %}.
- + {% endif %} diff --git a/hosting/urls.py b/hosting/urls.py index 5b2b87b0..e34d27d6 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -51,7 +51,7 @@ urlpatterns = [ name='choice_ssh_keys'), url(r'delete_ssh_key/(?P\d+)/?$', SSHKeyDeleteView.as_view(), name='delete_ssh_key'), - url(r'delete_card/(?P\d+)/?$', SettingsView.as_view(), + url(r'delete_card/(?P[\w\-]+)/$', SettingsView.as_view(), name='delete_card'), url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(), name='create_ssh_key'), diff --git a/hosting/views.py b/hosting/views.py index 729d115b..e2f6e13b 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,6 +1,7 @@ import logging import uuid from datetime import datetime +from urllib.parse import quote from time import sleep import stripe @@ -13,6 +14,7 @@ from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse_lazy, reverse +from django.db.models import Q from django.http import ( Http404, HttpResponseRedirect, HttpResponse, JsonResponse ) @@ -386,7 +388,7 @@ class PasswordResetConfirmView(HostingContextMixin, user = CustomUser.objects.get(pk=uid) opennebula_client = OpenNebulaManager( - email=user.email, + email=user.username, password=user.password, ) @@ -478,7 +480,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView): def delete(self, request, *args, **kwargs): owner = self.request.user manager = OpenNebulaManager( - email=owner.email, + email=owner.username, password=owner.password ) pk = self.kwargs.get('pk') @@ -532,7 +534,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View): ssh_key.private_key.save(filename, content) owner = self.request.user manager = OpenNebulaManager( - email=owner.email, + email=owner.username, password=owner.password ) keys = get_all_public_keys(request.user) @@ -563,9 +565,11 @@ class SettingsView(LoginRequiredMixin, FormView): stripe_customer = None if hasattr(user, 'stripecustomer'): stripe_customer = user.stripecustomer - cards_list = UserCardDetail.get_all_cards_list( - stripe_customer=stripe_customer + 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, 'stripe_key': settings.STRIPE_API_PUBLIC_KEY @@ -576,48 +580,38 @@ class SettingsView(LoginRequiredMixin, FormView): def post(self, request, *args, **kwargs): if 'card' in request.POST and request.POST['card'] is not '': card_id = escape(request.POST['card']) - user_card_detail = UserCardDetail.objects.get(id=card_id) UserCardDetail.set_default_card( stripe_api_cus_id=request.user.stripecustomer.stripe_id, - stripe_source_id=user_card_detail.card_id + stripe_source_id=card_id ) + stripe_utils = StripeUtils() + card_details = stripe_utils.get_cards_details_from_payment_method( + card_id + ) + if not card_details.get('response_object'): + logger.debug("Could not find card %s in stripe" % card_id) + messages.add_message(request, messages.ERROR, + _("Could not set a default card.")) + return HttpResponseRedirect(reverse_lazy('hosting:settings')) + card_details_response = card_details['response_object'] msg = _( ("Your {brand} card ending in {last4} set as " "default card").format( - brand=user_card_detail.brand, - last4=user_card_detail.last4 + brand=card_details_response['brand'], + last4=card_details_response['last4'] ) ) messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse_lazy('hosting:settings')) if 'delete_card' in request.POST: - try: - card = UserCardDetail.objects.get(pk=self.kwargs.get('pk')) - if (request.user.has_perm(self.permission_required[0], card) - and - request.user - .stripecustomer - .usercarddetail_set - .count() > 1): - if card.card_id is not None: - stripe_utils = StripeUtils() - stripe_utils.dissociate_customer_card( - request.user.stripecustomer.stripe_id, - card.card_id - ) - if card.preferred: - UserCardDetail.set_default_card_from_stripe( - request.user.stripecustomer.stripe_id - ) - card.delete() - msg = _("Card deassociation successful") - messages.add_message(request, messages.SUCCESS, msg) - else: - msg = _("You are not permitted to do this operation") - messages.add_message(request, messages.ERROR, msg) - except UserCardDetail.DoesNotExist: - msg = _("The selected card does not exist") - messages.add_message(request, messages.ERROR, msg) + card = self.kwargs.get('pk') + stripe_utils = StripeUtils() + stripe_utils.dissociate_customer_card( + request.user.stripecustomer.stripe_id, + card + ) + msg = _("Card deassociation successful") + messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse_lazy('hosting:settings')) form = self.get_form() if form.is_valid(): @@ -692,51 +686,49 @@ class SettingsView(LoginRequiredMixin, FormView): msg = _("Billing address updated successfully") messages.add_message(request, messages.SUCCESS, msg) else: - token = form.cleaned_data.get('token') + id_payment_method = request.POST.get('id_payment_method', None) stripe_utils = StripeUtils() - card_details = stripe_utils.get_cards_details_from_token( - token + card_details = stripe_utils.get_cards_details_from_payment_method( + id_payment_method ) if not card_details.get('response_object'): form.add_error("__all__", card_details.get('error')) return self.render_to_response(self.get_context_data()) stripe_customer = StripeCustomer.get_or_create( - email=request.user.email, token=token + email=request.user.email, id_payment_method=id_payment_method ) card = card_details['response_object'] - if UserCardDetail.get_user_card_details(stripe_customer, card): - msg = _('You seem to have already added this card') - messages.add_message(request, messages.ERROR, msg) - else: - acc_result = stripe_utils.associate_customer_card( - request.user.stripecustomer.stripe_id, token - ) - if acc_result['response_object'] is None: - msg = _( - 'An error occurred while associating the card.' - ' Details: {details}'.format( - details=acc_result['error'] - ) - ) - messages.add_message(request, messages.ERROR, msg) - return self.render_to_response(self.get_context_data()) - preferred = False - if stripe_customer.usercarddetail_set.count() == 0: - preferred = True - UserCardDetail.create( - stripe_customer=stripe_customer, - last4=card['last4'], - brand=card['brand'], - fingerprint=card['fingerprint'], - exp_month=card['exp_month'], - exp_year=card['exp_year'], - card_id=card['card_id'], - preferred=preferred - ) + acc_result = stripe_utils.associate_customer_card( + request.user.stripecustomer.stripe_id, + id_payment_method, + set_as_default=True + ) + if acc_result['response_object'] is None: msg = _( - "Successfully associated the card with your account" + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) ) - messages.add_message(request, messages.SUCCESS, msg) + messages.add_message(request, messages.ERROR, msg) + return self.render_to_response(self.get_context_data()) + preferred = False + if stripe_customer.usercarddetail_set.count() == 0: + preferred = True + UserCardDetail.create( + stripe_customer=stripe_customer, + last4=card['last4'], + brand=card['brand'], + fingerprint=card['fingerprint'], + exp_month=card['exp_month'], + exp_year=card['exp_year'], + card_id=card['card_id'], + preferred=preferred + ) + msg = _( + "Successfully associated the card with your account" + ) + messages.add_message(request, messages.SUCCESS, msg) return self.render_to_response(self.get_context_data()) else: billing_address_data = form.cleaned_data @@ -952,7 +944,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): except VMDetail.DoesNotExist: try: manager = OpenNebulaManager( - email=owner.email, password=owner.password + email=owner.username, password=owner.password ) vm = manager.get_vm(obj.vm_id) context['vm'] = VirtualMachineSerializer(vm).data @@ -1077,7 +1069,13 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): billing_address_data = request.session.get('billing_address_data') vm_template_id = template.get('id', 1) stripe_api_cus_id = request.user.stripecustomer.stripe_id - if 'token' in self.request.session: + logger.debug("template=%s specs=%s stripe_customer_id=%s " + "billing_address_data=%s vm_template_id=%s " + "stripe_api_cus_id=%s" % ( + template, specs, stripe_customer_id, billing_address_data, + vm_template_id, stripe_api_cus_id) + ) + if 'id_payment_method' in self.request.session: card_details = stripe_utils.get_cards_details_from_token( request.session['token'] ) @@ -1094,7 +1092,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): ) if not ucd: acc_result = stripe_utils.associate_customer_card( - stripe_api_cus_id, request.session['token'], + stripe_api_cus_id, request.session['id_payment_method'], set_as_default=True ) if acc_result['response_object'] is None: @@ -1185,10 +1183,31 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): subscription_result = stripe_utils.subscribe_customer_to_plan( stripe_api_cus_id, [{"plan": stripe_plan.get('response_object').stripe_plan_id}], - coupon='ipv6-discount-8chf' if 'name' in discount and 'ipv6' in discount['name'].lower() else "", + 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') + latest_invoice = stripe.Invoice.retrieve(stripe_subscription_obj.latest_invoice) + ret = stripe.PaymentIntent.confirm( + latest_invoice.payment_intent + ) + if ret.status == 'requires_source_action' or ret.status == 'requires_action': + pi = stripe.PaymentIntent.retrieve( + latest_invoice.payment_intent + ) + context = { + 'sid': stripe_subscription_obj.id, + 'payment_intent_secret': pi.client_secret, + 'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY, + 'showSCA': True + } + return JsonResponse(context) # Check if the subscription was approved and is active if (stripe_subscription_obj is None or stripe_subscription_obj.status != 'active'): @@ -1227,6 +1246,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): user = { 'name': self.request.user.name, 'email': self.request.user.email, + 'username': self.request.user.username, 'pass': self.request.user.password, 'request_scheme': request.scheme, 'request_host': request.get_host(), @@ -1281,10 +1301,11 @@ class InvoiceListView(LoginRequiredMixin, TemplateView): page = self.request.GET.get('page', 1) context = super(InvoiceListView, self).get_context_data(**kwargs) invs_page = None + invs_page_charges = None if ('user_email' in self.request.GET and self.request.user.email == settings.ADMIN_EMAIL): user_email = self.request.GET['user_email'] - context['user_email'] = user_email + context['user_email'] = '%s' % quote(user_email) logger.debug( "user_email = {}".format(user_email) ) @@ -1294,7 +1315,8 @@ class InvoiceListView(LoginRequiredMixin, TemplateView): logger.debug("User does not exist") cu = self.request.user invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id, - count=100) + count=100, + status='paid') paginator = Paginator(invs.data, 10) try: invs_page = paginator.page(page) @@ -1302,6 +1324,21 @@ class InvoiceListView(LoginRequiredMixin, TemplateView): invs_page = paginator.page(1) except EmptyPage: invs_page = paginator.page(paginator.num_pages) + hosting_orders = HostingOrder.objects.filter( + customer=cu.stripecustomer).filter( + Q(subscription_id=None) | Q(subscription_id='') + ).order_by('-created_at') + stripe_chgs = [] + for ho in hosting_orders: + stripe_chgs.append({ho.id: stripe.Charge.retrieve(ho.stripe_charge_id)}) + + paginator_charges = Paginator(stripe_chgs, 10) + try: + invs_page_charges = paginator_charges.page(page) + except PageNotAnInteger: + invs_page_charges = paginator_charges.page(1) + except EmptyPage: + invs_page_charges = paginator_charges.page(paginator_charges.num_pages) else: try: invs = stripe.Invoice.list( @@ -1315,10 +1352,27 @@ class InvoiceListView(LoginRequiredMixin, TemplateView): invs_page = paginator.page(1) except EmptyPage: invs_page = paginator.page(paginator.num_pages) + hosting_orders = HostingOrder.objects.filter( + customer=self.request.user.stripecustomer).filter( + Q(subscription_id=None) | Q(subscription_id='') + ).order_by('-created_at') + stripe_chgs = [] + for ho in hosting_orders: + stripe_chgs.append( + {ho: stripe.Charge.retrieve(ho.stripe_charge_id)}) + paginator_charges = Paginator(stripe_chgs, 10) + try: + invs_page_charges = paginator_charges.page(page) + except PageNotAnInteger: + invs_page_charges = paginator_charges.page(1) + except EmptyPage: + invs_page_charges = paginator_charges.page( + paginator_charges.num_pages) except Exception as ex: logger.error(str(ex)) invs_page = None context["invs"] = invs_page + context["invs_charge"] = invs_page_charges return context @method_decorator(decorators) @@ -1399,7 +1453,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): # fallback to get it from the infrastructure try: manager = OpenNebulaManager( - email=self.request.user.email, + email=self.request.user.username, password=self.request.user.password ) vm = manager.get_vm(vm_id) @@ -1482,7 +1536,7 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): def get_queryset(self): owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) try: queryset = manager.get_vms() @@ -1643,7 +1697,7 @@ class VirtualMachineView(LoginRequiredMixin, View): owner = self.request.user vm = None manager = OpenNebulaManager( - email=owner.email, + email=owner.username, password=owner.password ) vm_id = self.kwargs.get('pk') @@ -1727,7 +1781,7 @@ class VirtualMachineView(LoginRequiredMixin, View): vm = self.get_object() manager = OpenNebulaManager( - email=owner.email, + email=owner.username, password=owner.password ) try: @@ -1755,7 +1809,7 @@ class VirtualMachineView(LoginRequiredMixin, View): error_msg = result.get('error') logger.error( 'Error canceling subscription for {user} and vm id ' - '{vm_id}'.format(user=owner.email, vm_id=vm.id) + '{vm_id}'.format(user=owner.username, vm_id=vm.id) ) logger.error(error_msg) admin_email_body['stripe_error_msg'] = error_msg @@ -1777,7 +1831,7 @@ class VirtualMachineView(LoginRequiredMixin, View): ) response['text'] = str(_('Error terminating VM')) + str(vm.id) else: - for t in range(15): + for t in range(settings.MAX_TIME_TO_WAIT_FOR_VM_TERMINATE): try: manager.get_vm(vm.id) except WrongIdError: @@ -1800,6 +1854,10 @@ class VirtualMachineView(LoginRequiredMixin, View): ) break else: + logger.debug( + 'Sleeping 2 seconds for terminate action on VM %s' % + vm.id + ) sleep(2) if not response['status']: response['text'] = str(_("VM terminate action timed out. " @@ -1827,6 +1885,7 @@ class VirtualMachineView(LoginRequiredMixin, View): email.send() admin_email_body.update(response) admin_email_body["customer_email"] = owner.email + admin_email_body["customer_username"] = owner.username admin_email_body["VM_ID"] = vm.id admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if hosting_order is not None @@ -1844,15 +1903,15 @@ class VirtualMachineView(LoginRequiredMixin, View): ) admin_email_body["subscription_amount"] = total_amount/100 admin_email_body["subscription_detail"] = content - admin_msg_sub = "VM and Subscription for VM {} and user: {}".format( + admin_msg_sub = "VM and Subscription for VM {} and user: {}, {}".format( vm.id, - owner.email + owner.email, owner.username ) email_to_admin_data = { 'subject': ("Deleted " if response['status'] else "ERROR deleting ") + admin_msg_sub, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], + 'to': ['dcl-orders@ungleich.ch'], 'body': "\n".join( ["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]), } @@ -1892,7 +1951,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, context = super(DetailView, self).get_context_data(**kwargs) owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) # Get vms queryset = manager.get_vms() diff --git a/membership/models.py b/membership/models.py index 80aaf408..81054fb9 100644 --- a/membership/models.py +++ b/membership/models.py @@ -233,8 +233,17 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): ldap_manager.create_user(self.username, password=password, firstname=first_name, lastname=last_name, email=self.email) - self.in_ldap = True - self.save() + else: + # User exists already in LDAP, but with a dummy credential + # We are here implies that the user has successfully + # authenticated against Django db, and a corresponding user + # exists in LDAP. + # We just update the LDAP credentials once again, assuming it + # was set to a dummy value while migrating users from Django to + # LDAP + ldap_manager.change_password(self.username, password) + self.in_ldap = True + self.save() def __str__(self): # __unicode__ on Python 2 return self.email @@ -268,7 +277,7 @@ class StripeCustomer(models.Model): return "%s - %s" % (self.stripe_id, self.user.email) @classmethod - def create_stripe_api_customer(cls, email=None, token=None, + def create_stripe_api_customer(cls, email=None, id_payment_method=None, customer_name=None): """ This method creates a Stripe API customer with the given @@ -279,7 +288,8 @@ class StripeCustomer(models.Model): stripe user. """ stripe_utils = StripeUtils() - stripe_data = stripe_utils.create_customer(token, email, customer_name) + stripe_data = stripe_utils.create_customer( + id_payment_method, email, customer_name) if stripe_data.get('response_object'): stripe_cus_id = stripe_data.get('response_object').get('id') return stripe_cus_id @@ -287,7 +297,7 @@ class StripeCustomer(models.Model): return None @classmethod - def get_or_create(cls, email=None, token=None): + def get_or_create(cls, email=None, token=None, id_payment_method=None): """ Check if there is a registered stripe customer with that email or create a new one diff --git a/opennebula_api/models.py b/opennebula_api/models.py index 19e3e4f7..2f76f423 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -154,6 +154,8 @@ class OpenNebulaManager(): protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError + except Exception as ex: + logger.error(str(ex)) def _get_user_pool(self): try: @@ -427,8 +429,12 @@ class OpenNebulaManager(): template_id = int(template_id) try: template_pool = self._get_template_pool() + if template_id in settings.UPDATED_TEMPLATES_DICT.keys(): + template_id = settings.UPDATED_TEMPLATES_DICT[template_id] return template_pool.get_by_id(template_id) - except: + except Exception as ex: + logger.debug("Template Id we are looking for : %s" % template_id) + logger.error(str(ex)) raise ConnectionRefusedError def create_template(self, name, cores, memory, disk_size, core_price, diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py index c7418aa5..34cdde7c 100644 --- a/opennebula_api/serializers.py +++ b/opennebula_api/serializers.py @@ -86,7 +86,7 @@ class VirtualMachineSerializer(serializers.Serializer): } try: - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password, ) opennebula_id = manager.create_vm(template_id=template_id, diff --git a/opennebula_api/views.py b/opennebula_api/views.py index 9bf03a74..318fa32e 100644 --- a/opennebula_api/views.py +++ b/opennebula_api/views.py @@ -19,7 +19,7 @@ class VmCreateView(generics.ListCreateAPIView): def get_queryset(self): owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) # We may have ConnectionRefusedError if we don't have a # connection to OpenNebula. For now, we raise ServiceUnavailable @@ -42,7 +42,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self): owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) # We may have ConnectionRefusedError if we don't have a # connection to OpenNebula. For now, we raise ServiceUnavailable @@ -54,7 +54,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView): def get_object(self): owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) # We may have ConnectionRefusedError if we don't have a # connection to OpenNebula. For now, we raise ServiceUnavailable @@ -66,7 +66,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView): def perform_destroy(self, instance): owner = self.request.user - manager = OpenNebulaManager(email=owner.email, + manager = OpenNebulaManager(email=owner.username, password=owner.password) # We may have ConnectionRefusedError if we don't have a # connection to OpenNebula. For now, we raise ServiceUnavailable diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..535cc7d4 --- /dev/null +++ b/release.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Nico Schottelius, 2021-12-17 + +current=$(git describe --dirty) +last_tag=$(git describe --tags --abbrev=0) +registry=harbor.ungleich.svc.p10.k8s.ooo/ungleich-public +image_url=$registry/dynamicweb:${current} + +if echo $current | grep -q -e 'dirty$'; then + echo Refusing to release a dirty tree build + exit 1 +fi + +if [ "$current" != "$last_tag" ]; then + echo "Last tag ($last_tag) is not current version ($current)" + echo "Only release proper versions" + exit 1 +fi + +docker tag dynamicweb:${current} ${image_url} +docker push ${image_url} diff --git a/requirements.txt b/requirements.txt index e7769a7e..8d04a189 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ django-compressor==2.0 django-debug-toolbar==1.4 python-dotenv==0.10.3 django-extensions==1.6.7 -django-filer==1.2.0 +django-filer==2.1.2 django-filter==0.13.0 django-formtools==1.0 django-guardian==1.4.4 @@ -83,7 +83,7 @@ stripe==2.41.0 wheel==0.29.0 django-admin-honeypot==1.0.0 coverage==4.3.4 -git+https://github.com/ungleich/python-oca.git#egg=python-oca +git+https://github.com/ungleich/python-oca.git#egg=oca djangorestframework==3.6.3 flake8==3.3.0 python-memcached==1.58 diff --git a/templates/gdpr/gdpr_banner.html b/templates/gdpr/gdpr_banner.html index 7e9f5c7f..f927f8ee 100644 --- a/templates/gdpr/gdpr_banner.html +++ b/templates/gdpr/gdpr_banner.html @@ -134,8 +134,6 @@ digitalglarus.ch
hack4lgarus.ch
ipv6onlyhosting.com
- ipv6onlyhosting.ch
- ipv6onlyhosting.net
django-hosting.ch
rails-hosting.ch
node-hosting.ch
diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 7bff9a89..b9e2eb8a 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -3,6 +3,8 @@ import logging import math import subprocess +from django.conf import settings + from oca.pool import WrongIdError from datacenterlight.models import VMPricing @@ -79,7 +81,8 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'): price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) + (decimal.Decimal(memory) * pricing.ram_unit_price) + (decimal.Decimal(disk_size) * pricing.ssd_unit_price) + - (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)) + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE)) cents = decimal.Decimal('.01') price = price.quantize(cents, decimal.ROUND_HALF_UP) return round(float(price), 2) @@ -102,7 +105,8 @@ def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0, (decimal.Decimal(cpu) * pricing.cores_unit_price) + (decimal.Decimal(memory) * pricing.ram_unit_price) + (decimal.Decimal(ssd_size) * pricing.ssd_unit_price) + - (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE) ) discount_name = pricing.discount_name @@ -118,7 +122,8 @@ def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0, discount = { 'name': discount_name, 'amount': discount_amount, - 'amount_with_vat': round(float(discount_amount_with_vat), 2) + 'amount_with_vat': round(float(discount_amount_with_vat), 2), + 'stripe_coupon_id': pricing.stripe_coupon_id } return (round(float(price), 2), round(float(vat), 2), round(float(vat_percent), 2), discount) @@ -154,7 +159,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, (decimal.Decimal(cpu) * pricing.cores_unit_price) + (decimal.Decimal(memory) * pricing.ram_unit_price) + (decimal.Decimal(ssd_size) * pricing.ssd_unit_price) + - (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE) ) if pricing.vat_inclusive: vat = decimal.Decimal(0) @@ -168,7 +174,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, vat = vat.quantize(cents, decimal.ROUND_HALF_UP) discount = { 'name': pricing.discount_name, - 'amount': round(float(pricing.discount_amount), 2) + 'amount': round(float(pricing.discount_amount), 2), + 'stripe_coupon_id': pricing.stripe_coupon_id } return (round(float(price), 2), round(float(vat), 2), round(float(vat_percent), 2), discount) @@ -215,11 +222,6 @@ def get_ip_addresses(vm_id): return "--" -def round_up(n, decimals=0): - multiplier = 10 ** decimals - return math.ceil(n * multiplier) / multiplier - - class HostingUtils: @staticmethod def clear_items_from_list(from_list, items_list): diff --git a/utils/ldap_manager.py b/utils/ldap_manager.py index fd039ad5..d40e931f 100644 --- a/utils/ldap_manager.py +++ b/utils/ldap_manager.py @@ -3,6 +3,7 @@ import hashlib import random import ldap3 import logging +import unicodedata from django.conf import settings @@ -87,7 +88,7 @@ class LdapManager: logger.debug("{uid} does not exist. Using it".format(uid=uidNumber)) self._set_max_uid(uidNumber) try: - uid = user.encode("utf-8") + uid = user conn.add("uid={uid},{customer_dn}".format( uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN ), @@ -101,7 +102,7 @@ class LdapManager: "uidNumber": [str(uidNumber)], "gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)], "loginShell": ["/bin/bash"], - "homeDirectory": ["/home/{}".format(user).encode("utf-8")], + "homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))], "mail": email.encode("utf-8"), "userPassword": [self._ssha_password( password.encode("utf-8") @@ -266,7 +267,7 @@ class LdapManager: logger.error( "Error reading int value from {}. {}" "Returning default value {} instead".format( - settings.LDAP_MAX_UID_PATH, + settings.LDAP_MAX_UID_FILE_PATH, str(ve), settings.LDAP_DEFAULT_START_UID ) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index ade06dd3..875a174e 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -34,6 +34,7 @@ def handleStripeError(f): logger.error(str(e)) return response except stripe.error.RateLimitError as e: + logger.error(str(e)) response.update( {'error': "Too many requests made to the API too quickly"}) return response @@ -69,7 +70,7 @@ class StripeUtils(object): CURRENCY = 'chf' INTERVAL = 'month' SUCCEEDED_STATUS = 'succeeded' - STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists' + RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists' STRIPE_NO_SUCH_PLAN = 'No such plan' PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.' PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.' @@ -82,20 +83,31 @@ class StripeUtils(object): customer.save() @handleStripeError - def associate_customer_card(self, stripe_customer_id, token, + def associate_customer_card(self, stripe_customer_id, id_payment_method, set_as_default=False): customer = stripe.Customer.retrieve(stripe_customer_id) - card = customer.sources.create(source=token) + stripe.PaymentMethod.attach( + id_payment_method, + customer=stripe_customer_id, + ) if set_as_default: - customer.default_source = card.id + customer.invoice_settings.default_payment_method = id_payment_method customer.save() return True @handleStripeError def dissociate_customer_card(self, stripe_customer_id, card_id): customer = stripe.Customer.retrieve(stripe_customer_id) - card = customer.sources.retrieve(card_id) - card.delete() + if card_id.startswith("pm"): + logger.debug("PaymentMethod %s detached %s" % (card_id, + stripe_customer_id)) + pm = stripe.PaymentMethod.retrieve(card_id) + stripe.PaymentMethod.detach(card_id) + pm.delete() + else: + logger.debug("card %s detached %s" % (card_id, stripe_customer_id)) + card = customer.sources.retrieve(card_id) + card.delete() @handleStripeError def update_customer_card(self, customer_id, token): @@ -187,6 +199,24 @@ class StripeUtils(object): } return card_details + @handleStripeError + def get_cards_details_from_payment_method(self, payment_method_id): + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + # payment_method does not always seem to have a card with id + # if that is the case, fallback to payment_method_id for card_id + card_id = payment_method_id + if hasattr(payment_method.card, 'id'): + card_id = payment_method.card.id + card_details = { + 'last4': payment_method.card.last4, + 'brand': payment_method.card.brand, + 'exp_month': payment_method.card.exp_month, + 'exp_year': payment_method.card.exp_year, + 'fingerprint': payment_method.card.fingerprint, + 'card_id': card_id + } + return card_details + def check_customer(self, stripe_cus_api_id, user, token): try: customer = stripe.Customer.retrieve(stripe_cus_api_id) @@ -206,11 +236,11 @@ class StripeUtils(object): return customer @handleStripeError - def create_customer(self, token, email, name=None): + def create_customer(self, id_payment_method, email, name=None): if name is None or name.strip() == "": name = email customer = self.stripe.Customer.create( - source=token, + payment_method=id_payment_method, description=name, email=email ) @@ -267,11 +297,17 @@ class StripeUtils(object): stripe_plan_db_obj = StripePlan.objects.create( stripe_plan_id=stripe_plan_id) except stripe.error.InvalidRequestError as e: - if self.STRIPE_PLAN_ALREADY_EXISTS in str(e): + logger.error(str(e)) + logger.error("error_code = %s" % str(e.__dict__)) + if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code: logger.debug( self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) - stripe_plan_db_obj = StripePlan.objects.create( + stripe_plan_db_obj, c = StripePlan.objects.get_or_create( stripe_plan_id=stripe_plan_id) + if c: + logger.debug("Created stripe plan %s" % stripe_plan_id) + else: + logger.debug("Plan %s exists already" % stripe_plan_id) return stripe_plan_db_obj @handleStripeError @@ -300,10 +336,14 @@ class StripeUtils(object): @handleStripeError def subscribe_customer_to_plan(self, customer, plans, trial_end=None, - coupon="", tax_rates=list()): + coupon="", tax_rates=list(), + default_payment_method=""): """ Subscribes the given customer to the list of given plans + :param default_payment_method: + :param tax_rates: + :param coupon: :param customer: The stripe customer identifier :param plans: A list of stripe plans. :param trial_end: An integer representing when the Stripe subscription @@ -317,12 +357,17 @@ class StripeUtils(object): ] :return: The subscription StripeObject """ - + logger.debug("Subscribing %s to plan %s : coupon = %s" % ( + customer, str(plans), str(coupon) + )) subscription_result = self.stripe.Subscription.create( customer=customer, items=plans, trial_end=trial_end, coupon=coupon, default_tax_rates=tax_rates, + payment_behavior='allow_incomplete', + default_payment_method=default_payment_method ) + logger.debug("Done subscribing") return subscription_result @handleStripeError @@ -480,7 +525,49 @@ class StripeUtils(object): ) return tax_id_obj + @handleStripeError + def get_payment_intent(self, amount, customer): + """ Create a stripe PaymentIntent of the given amount and return it + :param amount: the amount of payment_intent + :return: + """ + payment_intent_obj = stripe.PaymentIntent.create( + amount=amount, + currency='chf', + customer=customer, + setup_future_usage='off_session' + ) + return payment_intent_obj + + @handleStripeError + def get_available_payment_methods(self, customer): + """ Retrieves all payment methods of the given customer + :param customer: StripeCustomer object + :return: a list of available payment methods + """ + return_list = [] + if customer is None: + return return_list + cu = stripe.Customer.retrieve(customer.stripe_id) + pms = stripe.PaymentMethod.list( + customer=customer.stripe_id, + type="card", + ) + default_source = None + if cu.default_source: + default_source = cu.default_source + else: + default_source = cu.invoice_settings.default_payment_method + for pm in pms.data: + return_list.append({ + 'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id, + 'exp_year': pm.card.exp_year, + 'exp_month': '{:02d}'.format(pm.card.exp_month), + 'preferred': pm.id == default_source + }) + return return_list + def compare_vat_numbers(self, vat1, vat2): _vat1 = vat1.replace(" ", "").replace(".", "").replace("-","") _vat2 = vat2.replace(" ", "").replace(".", "").replace("-","") - return True if _vat1 == _vat2 else False \ No newline at end of file + return True if _vat1 == _vat2 else False diff --git a/utils/views.py b/utils/views.py index 05d0fdc2..f30a349a 100644 --- a/utils/views.py +++ b/utils/views.py @@ -228,7 +228,7 @@ class SSHKeyCreateView(FormView): if self.request.user.is_authenticated(): owner = self.request.user manager = OpenNebulaManager( - email=owner.email, + email=owner.username, password=owner.password ) keys_to_save = get_all_public_keys(self.request.user) diff --git a/vat_rates.csv b/vat_rates.csv index 17bdb997..72870530 100644 --- a/vat_rates.csv +++ b/vat_rates.csv @@ -321,5 +321,4 @@ IM",GBP,0.1,standard, 2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually) 2019-12-17,,FX,EUR,0.20,standard,France metropolitan standard VAT (added manually) 2020-01-04,,CY,EUR,0.19,standard,Cyprus standard VAT (added manually) -2019-01-04,,IL,EUR,0.23,standard,Ireland standard VAT (added manually) 2019-01-04,,LI,EUR,0.077,standard,Liechtenstein standard VAT (added manually) diff --git a/webhook/views.py b/webhook/views.py index 516d1afc..0a96d0b6 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -1,14 +1,17 @@ import datetime import logging - +import json import stripe + # Create your views here. from django.conf import settings from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST +from datacenterlight.views import do_provisioning, do_provisioning_generic from membership.models import StripeCustomer +from hosting.models import IncompleteSubscriptions, IncompletePaymentIntents from utils.models import BillingAddress, UserBillingAddress from utils.tasks import send_plain_email_task @@ -111,8 +114,193 @@ def handle_webhook(request): 'to': settings.DCL_ERROR_EMAILS_TO_LIST, 'body': "Response = %s" % str(tax_id_obj), } - send_plain_email_task.delay(email_data) + elif event.type == 'invoice.paid': + #More info: https://stripe.com/docs/billing/migration/strong-customer-authentication#scenario-1-handling-fulfillment + invoice_obj = event.data.object + logger.debug("Webhook Event: invoice.paid") + logger.debug("invoice_obj %s " % str(invoice_obj)) + logger.debug("invoice_obj.paid = %s %s" % (invoice_obj.paid, type(invoice_obj.paid))) + logger.debug("invoice_obj.billing_reason = %s %s" % (invoice_obj.billing_reason, type(invoice_obj.billing_reason))) + # We should check for billing_reason == "subscription_create" but we + # check for "subscription_update" + # because we are using older api. + # See https://stripe.com/docs/upgrades?since=2015-07-13 + + # The billing_reason attribute of the invoice object now can take the + # value of subscription_create, indicating that it is the first + # invoice of a subscription. For older API versions, + # billing_reason=subscription_create is represented as + # subscription_update. + + if (invoice_obj.paid and + invoice_obj.billing_reason == "subscription_update"): + logger.debug("""invoice_obj.paid and + invoice_obj.billing_reason == subscription_update""") + logger.debug("Start provisioning") + try: + logger.debug("Looking for subscription %s" % + invoice_obj.subscription) + stripe_subscription_obj = stripe.Subscription.retrieve( + invoice_obj.subscription) + try: + incomplete_sub = IncompleteSubscriptions.objects.get( + subscription_id=invoice_obj.subscription) + request = "" + soc = "" + card_details_response = "" + gp_details = "" + template = "" + specs = "" + billing_address_data = "" + if incomplete_sub.request: + request = json.loads(incomplete_sub.request) + if incomplete_sub.specs: + specs = json.loads(incomplete_sub.specs) + if incomplete_sub.stripe_onetime_charge: + soc = json.loads(incomplete_sub.stripe_onetime_charge) + if incomplete_sub.gp_details: + gp_details = json.loads(incomplete_sub.gp_details) + if incomplete_sub.card_details_response: + card_details_response = json.loads( + incomplete_sub.card_details_response) + if incomplete_sub.template: + template = json.loads( + incomplete_sub.template) + if incomplete_sub.billing_address_data: + billing_address_data = json.loads( + incomplete_sub.billing_address_data) + logger.debug("*******") + logger.debug(str(incomplete_sub)) + logger.debug("*******") + logger.debug("1*******") + logger.debug(request) + logger.debug("2*******") + logger.debug(card_details_response) + logger.debug("3*******") + logger.debug(soc) + logger.debug("4*******") + logger.debug(gp_details) + logger.debug("5*******") + logger.debug(template) + logger.debug("6*******") + do_provisioning( + request=request, + stripe_api_cus_id=incomplete_sub.stripe_api_cus_id, + card_details_response=card_details_response, + stripe_subscription_obj=stripe_subscription_obj, + stripe_onetime_charge=soc, + gp_details=gp_details, + specs=specs, + vm_template_id=incomplete_sub.vm_template_id, + template=template, + billing_address_data=billing_address_data, + real_request=None + ) + except IncompleteSubscriptions.DoesNotExist as ex: + logger.error(str(ex)) + except IncompleteSubscriptions.MultipleObjectsReturned as ex: + logger.error(str(ex)) + email_data = { + 'subject': "IncompleteSubscriptions error", + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Response = %s" % str(ex), + } + send_plain_email_task.delay(email_data) + except Exception as ex: + logger.error(str(ex)) + email_data = { + 'subject': "invoice.paid Webhook error", + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Response = %s" % str(ex), + } + send_plain_email_task.delay(email_data) + elif event.type == 'invoice.payment_failed': + invoice_obj = event.data.object + logger.debug("Webhook Event: invoice.payment_failed") + logger.debug("invoice_obj %s " % str(invoice_obj)) + if (invoice_obj.payment_failed and + invoice_obj.billing_reason == "subscription_update"): + logger.debug("Payment failed, inform the users") + elif event.type == 'payment_intent.succeeded': + payment_intent_obj = event.data.object + logger.debug("Webhook Event: payment_intent.succeeded") + logger.debug("payment_intent_obj %s " % str(payment_intent_obj)) + try: + logger.debug("Looking for IncompletePaymentIntents %s " % + payment_intent_obj.id) + incomplete_pm = IncompletePaymentIntents.objects.get( + payment_intent_id=payment_intent_obj.id) + logger.debug("incomplete_pm = %s" % str(incomplete_pm.__dict__)) + request = "" + soc = "" + card_details_response = "" + gp_details = "" + template = "" + billing_address_data = "" + if incomplete_pm.request: + request = json.loads(incomplete_pm.request) + logger.debug("request = %s" % str(request)) + if incomplete_pm.stripe_charge_id: + soc = incomplete_pm.stripe_charge_id + logger.debug("stripe_onetime_charge = %s" % str(soc)) + if incomplete_pm.gp_details: + gp_details = json.loads(incomplete_pm.gp_details) + logger.debug("gp_details = %s" % str(gp_details)) + if incomplete_pm.card_details_response: + card_details_response = json.loads( + incomplete_pm.card_details_response) + logger.debug("card_details_response = %s" % str(card_details_response)) + if incomplete_pm.billing_address_data: + billing_address_data = json.loads( + incomplete_pm.billing_address_data) + logger.debug("billing_address_data = %s" % str(billing_address_data)) + logger.debug("1*******") + logger.debug(request) + logger.debug("2*******") + logger.debug(card_details_response) + logger.debug("3*******") + logger.debug(soc) + logger.debug("4*******") + logger.debug(gp_details) + logger.debug("5*******") + logger.debug(template) + logger.debug("6*******") + logger.debug(billing_address_data) + incomplete_pm.completed_at = datetime.datetime.now() + charges = "" + if len(payment_intent_obj.charges.data) > 0: + for d in payment_intent_obj.charges.data: + if charges == "": + charges = "%s" % d.id + else: + charges = "%s,%s" % (charges, d.id) + logger.debug("Charge ids = %s" % charges) + incomplete_pm.stripe_charge_id=charges + do_provisioning_generic( + request=request, + stripe_api_cus_id=incomplete_pm.stripe_api_cus_id, + card_details_response=card_details_response, + stripe_subscription_id=None, + stripe_charge_id=charges, + gp_details=gp_details, + billing_address_data=billing_address_data + ) + incomplete_pm.save() + except IncompletePaymentIntents.DoesNotExist as ex: + logger.error(str(ex)) + except (IncompletePaymentIntents.MultipleObjectsReturned, + Exception) as ex: + logger.error(str(ex)) + email_data = { + 'subject': "IncompletePaymentIntents error", + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Response = %s" % str(ex), + } + send_plain_email_task.delay(email_data) else: logger.error("Unhandled event : " + event.type) return HttpResponse(status=200)