diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a715c9d7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +.env diff --git a/.gitignore b/.gitignore index 2d923e99..a41d813d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ secret-key !.gitkeep *.orig .vscode/settings.json +2024-* +2025-* +2023-* diff --git a/Changelog b/Changelog index 5700852e..9e420c87 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,46 @@ +3.4: 2022-04-14 + * 11566: Fix for ungleich.ch product section alignment +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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4c1a9a66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# FROM python:3.10.0-alpine3.15 +FROM python:3.5-alpine3.12 + +WORKDIR /usr/src/app + +RUN apk add --update --no-cache \ + git \ + build-base \ + openldap-dev \ + python3-dev \ + postgresql-dev \ + jpeg-dev \ + libxml2-dev \ + libxslt-dev \ + libmemcached-dev \ + zlib-dev \ + && rm -rf /var/cache/apk/* + +## For alpine 3.15 replace postgresql-dev with libpq-dev + +# FIX https://github.com/python-ldap/python-ldap/issues/432 +RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so + +COPY requirements.txt ./ + +# Pillow seems to need LIBRARY_PATH set as follows: (see: https://github.com/python-pillow/Pillow/issues/1763#issuecomment-222383534) +RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install --no-cache-dir -r requirements.txt" + +COPY ./ . +COPY entrypoint.sh / + +ENTRYPOINT ["/entrypoint.sh" ] 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/alplora/views.py b/alplora/views.py index 0a10b4e0..d628cffc 100644 --- a/alplora/views.py +++ b/alplora/views.py @@ -31,9 +31,10 @@ class ContactView(FormView): return context def form_valid(self, form): - form.save() - form.send_email(email_to='info@alplora.ch') - messages.add_message(self.request, messages.SUCCESS, self.success_message) + print("alplora contactusform") + #form.save() + #form.send_email(email_to='info@alplora.ch') + #messages.add_message(self.request, messages.SUCCESS, self.success_message) return render(self.request, 'alplora/contact_success.html', {}) diff --git a/build-image.sh b/build-image.sh new file mode 100755 index 00000000..64a67fd6 --- /dev/null +++ b/build-image.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +if [ $# -lt 1 ]; then + echo "$0 imageversion [push]" + echo "Version could be: $(git describe --always)" + echo "If push is specified, also push to our harbor" + exit 1 +fi + +tagprefix=harbor.k8s.ungleich.ch/ungleich-public/dynamicweb +version=$1; shift + +tag=${tagprefix}:${version} + +set -ex + +docker build -t "${tag}" . + +push=$1; shift + +if [ "$push" ]; then + docker push "${tag}" +fi 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..d30e5b0d --- /dev/null +++ b/datacenterlight/management/commands/check_vm_templates.py @@ -0,0 +1,80 @@ +from django.core.management.base import BaseCommand +from opennebula_api.models import OpenNebulaManager +from datacenterlight.models import VMTemplate +from datacenterlight.cms_models import DCLCalculatorPluginModel +from membership.models import CustomUser +from utils.tasks import send_plain_email_task +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 = {} + error_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) + public_templates_plugin = DCLCalculatorPluginModel.objects.filter(cmsplugin_ptr_id=23356).first() + ipv6_templates_plugin = DCLCalculatorPluginModel.objects.filter(cmsplugin_ptr_id=21943).first() + templates = public_templates_plugin.vm_templates_to_show + ipv6_templates_plugin.vm_templates_to_show + vm_templates = VMTemplate.objects.filter(opennebula_vm_template_id__in=templates) + for vm_template in vm_templates: + vm_name = 'test-%s-%s' % (vm_template.vm_type, 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) + error_dict[vm_name] = result_dict[vm_name] + 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)) + if error_dict: + email_data = { + 'subject': 'Check VM Templates ERROR', + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': [settings.ADMIN_EMAIL], + 'body': json.dumps(error_dict), + } + send_plain_email_task.delay(email_data) + self.stdout.write(self.style.SUCCESS("Done")) diff --git a/datacenterlight/management/commands/fix_generic_stripe_plan_product_names.py b/datacenterlight/management/commands/fix_generic_stripe_plan_product_names.py new file mode 100644 index 00000000..2773009d --- /dev/null +++ b/datacenterlight/management/commands/fix_generic_stripe_plan_product_names.py @@ -0,0 +1,54 @@ +from django.core.management.base import BaseCommand +from datacenterlight.tasks import handle_metadata_and_emails +from datacenterlight.models import StripePlan +from opennebula_api.models import OpenNebulaManager +from membership.models import CustomUser +from hosting.models import GenericProduct +import logging +import json +import sys +import stripe + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = '''Stripe plans created before version 3.4 saved the plan name like generic-{subscription_id}-amount. This + command aims at replacing this with the actual product name + ''' + + def handle(self, *args, **options): + cnt = 0 + self.stdout.write( + self.style.SUCCESS( + 'In Fix generic stripe plan product names' + ) + ) + plans_to_change = StripePlan.objects.filter(stripe_plan_id__startswith='generic') + for plan in plans_to_change: + response = input("Press 'y' to continue: ") + + # Check if the user entered 'y' + if response.lower() == 'y': + plan_name = plan.stripe_plan_id + first_index_hyphen = plan_name.index("-") + 1 + product_id = plan_name[ + first_index_hyphen:(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen] + gp = GenericProduct.objects.get(id=product_id) + if gp: + cnt += 1 + # update stripe + sp = stripe.Plan.retrieve(plan_name) + pr = stripe.Product.retrieve(sp.product) + pr.name = gp.product_name + pr.save() + # update local + spl = StripePlan.objects.get(stripe_plan_id=plan_name) + spl.stripe_plan_name = gp.product_name + spl.save() + print("%s. %s => %s" % (cnt, plan_name, gp.product_name)) + else: + print("Invalid input. Please try again.") + sys.exit() + + print("Done") \ No newline at end of file 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 f080da90..899b506f 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -56,11 +56,6 @@ 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('username') on_pass = user.get('pass') @@ -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..fc39eec7 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,42 @@ 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: + print("Hositng order id = %s" % hosting_order_id) + 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 +115,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 @@ -109,7 +145,7 @@ def get_line_item_from_stripe_invoice(invoice): """.format( vm_id=vm_id if vm_id > 0 else "", ip_addresses=mark_safe(get_ip_addresses(vm_id)) if vm_id > 0 else - mark_safe(get_product_name(plan_name)), + mark_safe(get_product_name(plan_name)) if plan_name.startswith("generic-") else plan_name, period=mark_safe("%s — %s" % ( datetime.datetime.fromtimestamp(start_date).strftime('%Y-%m-%d'), datetime.datetime.fromtimestamp(end_date).strftime('%Y-%m-%d'))), @@ -125,8 +161,7 @@ def get_product_name(plan_name): product_name = "" if plan_name and plan_name.startswith("generic-"): first_index_hyphen = plan_name.index("-") + 1 - product_id = plan_name[first_index_hyphen: - (plan_name[first_index_hyphen:].index("-")) + first_index_hyphen] + product_id = plan_name[first_index_hyphen:(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen] try: product = GenericProduct.objects.get(id=product_id) product_name = product.product_name 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 aefaf6a8..c708ff6a 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 @@ -40,6 +42,8 @@ from .utils import ( get_cms_integration, create_vm, clear_all_session_vars, validate_vat_number ) +from datacenterlight.templatetags.custom_tags import get_product_name + logger = logging.getLogger(__name__) @@ -61,23 +65,23 @@ class ContactUsView(FormView): ) def form_valid(self, form): - form.save() - from_emails = { - 'glasfaser': 'glasfaser@ungleich.ch' - } - from_page = self.request.POST.get('from_page') - email_data = { - 'subject': "{dcl_text} Message from {sender}".format( - dcl_text=settings.DCL_TEXT, - sender=form.cleaned_data.get('email') - ), - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': [from_emails.get(from_page, 'support@ungleich.ch')], - 'body': "\n".join( - ["%s=%s" % (k, v) for (k, v) in form.cleaned_data.items()]), - 'reply_to': [form.cleaned_data.get('email')], - } - send_plain_email_task.delay(email_data) + #form.save() + #from_emails = { + # 'glasfaser': 'glasfaser@ungleich.ch' + #} + #from_page = self.request.POST.get('from_page') + #email_data = { + # 'subject': "{dcl_text} Message from {sender}".format( + # dcl_text=settings.DCL_TEXT, + # sender=form.cleaned_data.get('email') + # ), + # 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + # 'to': [from_emails.get(from_page, 'support@ungleich.ch')], + # 'body': "\n".join( + # ["%s=%s" % (k, v) for (k, v) in form.cleaned_data.items()]), + # 'reply_to': [form.cleaned_data.get('email')], + #} + #send_plain_email_task.delay(email_data) if self.request.is_ajax(): return self.render_to_response( self.get_context_data(success=True, contact_form=form)) @@ -197,6 +201,8 @@ class IndexView(CreateView): ssd_size=storage, pricing_name=vm_pricing_name ) + if request.user.id == 51: + print("User is test") specs = { 'cpu': cores, 'memory': memory, @@ -262,9 +268,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 +318,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 +438,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 +460,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 +472,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 +508,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 +519,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,25 +588,37 @@ 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 + print("******************************") + print("User = {}, ID = {}".format(request.user, request.user.id)) + print("******************************") + 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') - card_detail = UserCardDetail.objects.get(id=card_id) + logger.debug("NO id_payment_method, using card: %s" % card_id) + card_detail = UserCardDetail.get_ucd_from_card_id(card_id=card_id) context['cc_last4'] = card_detail.last4 context['cc_brand'] = card_detail.brand context['cc_exp_year'] = card_detail.exp_year @@ -624,11 +631,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 = ( @@ -642,11 +652,14 @@ class OrderConfirmationView(DetailView, FormView): pricing_name=vm_specs['pricing_name'], vat_rate=user_country_vat_rate * 100 ) + if request.user.id == 51: + print("User is test") 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 +673,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 +690,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 +704,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 +756,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 +831,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,38 +841,35 @@ 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) + user_card_detail = UserCardDetail.get_ucd_from_card_id(card_id=card_id) card_details_dict = { 'last4': user_card_detail.last4, 'brand': user_card_detail.brand, 'card_id': user_card_detail.card_id } + UserCardDetail.set_default_card( + stripe_api_cus_id=stripe_api_cus_id, + stripe_source_id=user_card_detail.card_id + ) + logger.debug("card_details_dict=%s" % card_details_dict) else: response = { 'status': False, @@ -808,44 +887,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'] @@ -859,15 +909,21 @@ class OrderConfirmationView(DetailView, FormView): 2 ) ) - plan_name = "generic-{0}-{1:.2f}".format( + stripe_plan_id = "generic-{0}-{1:.2f}".format( request.session['generic_payment_details']['product_id'], amount_to_be_charged ) - stripe_plan_id = plan_name + try: + product = GenericProduct.objects.get(id=request.session['generic_payment_details']['product_id']) + plan_name = product.product_name + except Exception as ex: + logger.debug("Errori {}" % str(ex)) + plan_name = get_product_name(stripe_plan_id) recurring_interval = request.session['generic_payment_details']['recurring_interval'] if recurring_interval == "year": - plan_name = "{}-yearly".format(plan_name) - stripe_plan_id = plan_name + stripe_plan_id = "{}-yearly".format(stripe_plan_id) + plan_name = "{} (yearly)".format(plan_name) + logger.debug("Plan name = {}, Stripe Plan id = {}".format(plan_name, stripe_plan_id)) else: template = request.session.get('template') specs = request.session.get('specs') @@ -894,6 +950,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,259 +990,140 @@ 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. # ... - + logger.debug("In 1 ***") + logger.debug("stripe_subscription_obj == %s" % stripe_subscription_obj) 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.username, - 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, - 'username': custom_user.username, - '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': ( @@ -1201,3 +1139,522 @@ 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): + logger.debug(":: set_user_card") + if card_id: + logger.debug("card_id %s was in request" % card_id) + user_card_detail = UserCardDetail.get_ucd_from_card_id(card_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 + ) + logger.debug(" ucd = %s" % ucd) + UserCardDetail.save_default_card_local( + custom_user.stripecustomer.stripe_id, + ucd.card_id + ) + logger.debug(" after save_default_card_local") + card_details_dict = { + 'last4': ucd.last4, + 'brand': ucd.brand, + 'card_id': ucd.card_id + } + logger.debug("card_detail_dict = %s" % card_details_dict) + 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") + + try: + params = { + 'request': request, + 'stripe_api_cus_id': stripe_api_cus_id, + 'card_details_response': card_details_response, + 'stripe_subscription_obj': stripe_subscription_obj, + 'stripe_onetime_charge': stripe_onetime_charge, + 'gp_details': gp_details, + 'specs': specs, + 'vm_template_id': vm_template_id, + 'template': template, + 'billing_address_data': billing_address_data, + 'real_request': real_request + } + print("Input Parameters:") + for key, value in params.items(): + try: + print("{}: {}".format(key, value)) + except Exception as e: + print("{}: [Error printing value: {}]".format(key, str(e))) + except Exception as e: + print("Error printing parameters: {}".format(str(e))) + + 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) + logger.debug("after set_user_card %s" % card_details_dict) + + # 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() + logger.debug("billing_address saved") + + order = HostingOrder.create( + price=request['generic_payment_details']['amount'], + customer=stripe_cus, + billing_address=billing_address, + vm_pricing=VMPricing.get_default_pricing() + ) + logger.debug("hosting order created") + + # 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']], + } + logger.debug("Sending email") + send_plain_email_task.delay(email_data) + logger.debug("After Sending email") + 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'], + } + logger.debug("Before Sending customer email") + 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.debug("Init get_error_response_dict {}".format(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/digitalglarus/views.py b/digitalglarus/views.py index 299327e6..bf3ed2c6 100644 --- a/digitalglarus/views.py +++ b/digitalglarus/views.py @@ -835,9 +835,10 @@ class ContactView(FormView): success_message = _('Message Successfully Sent') def form_valid(self, form): - form.save() - form.send_email() - messages.add_message(self.request, messages.SUCCESS, self.success_message) + print("digital glarus contactusform") + #form.save() + #form.send_email() + #messages.add_message(self.request, messages.SUCCESS, self.success_message) return super(ContactView, self).form_valid(form) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index c959c237..b7705e17 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -56,6 +56,9 @@ dotenv.load_dotenv("{0}/.env".format(PROJECT_DIR)) from multisite import SiteID +RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY') +RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PRIVATE_KEY') + UNGLEICH_BLOG_SITE_ID = int_env("UNGLEICH_BLOG_SITE_ID") SITE_ID = SiteID(default=(UNGLEICH_BLOG_SITE_ID if UNGLEICH_BLOG_SITE_ID > 0 else 1)) @@ -125,6 +128,7 @@ INSTALLED_APPS = ( 'djangocms_file', 'djangocms_picture', 'djangocms_video', + 'django_recaptcha', # 'djangocms_flash', # 'djangocms_googlemap', # 'djangocms_inherit', @@ -631,8 +635,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,8 +763,23 @@ 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 else: from .prod import * # flake8: noqa + +# Try to load dynamic configuration, if it exists +try: + from .dynamic import * # flake8: noqa +except ImportError: + pass 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/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..e207a457 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -uex + +cd /usr/src/app/ +cat > dynamicweb/settings/dynamic.py <\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/management/commands/change_ch_vatrate_2023.py b/hosting/management/commands/change_ch_vatrate_2023.py new file mode 100644 index 00000000..612b246e --- /dev/null +++ b/hosting/management/commands/change_ch_vatrate_2023.py @@ -0,0 +1,144 @@ +from django.core.management.base import BaseCommand +import datetime +import csv +import logging +import stripe +from hosting.models import VATRates +from utils.hosting_utils import get_vat_rate_for_country +from django.conf import settings +from membership.models import CustomUser, StripeCustomer + +stripe.api_key = settings.STRIPE_API_PRIVATE_KEY + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = '''CH vat rate changes on 2024-01-01 from 7.7% to 8.1%. This commands makes the necessary changes''' + + def handle(self, *args, **options): + MAKE_MODIFS=False + try: + country_to_change = 'CH' + currency_to_change = 'CHF' + new_rate = 0.081 + user_country_vat_rate = get_vat_rate_for_country(country_to_change) + logger.debug("Existing VATRate for %s %s " % (country_to_change, user_country_vat_rate)) + vat_rate = VATRates.objects.get( + territory_codes=country_to_change, start_date__isnull=False, stop_date=None + ) + logger.debug("VAT rate for %s is %s" % (country_to_change, vat_rate.rate)) + logger.debug("vat_rate object = %s" % vat_rate) + logger.debug("Create end date for the VATRate %s" % vat_rate.id) + # if MAKE_MODIFS: + # vat_rate.stop_date = datetime.date(2023, 12, 31) + # vat_rate.save() + # print("Creating a new VATRate for CH") + # obj, created = VATRates.objects.get_or_create( + # start_date=datetime.date(2024, 1, 1), + # stop_date=None, + # territory_codes=country_to_change, + # currency_code=currency_to_change, + # rate=new_rate, + # rate_type="standard", + # description="Switzerland standard VAT (added manually on %s)" % datetime.datetime.now() + # ) + # if created: + # logger.debug("Created new VAT Rate for %s with the new rate %s" % (country_to_change, new_rate)) + # logger.debug(obj) + # else: + # logger.debug("VAT Rate for %s already exists with the rate %s" % (country_to_change, new_rate)) + # + logger.debug("Getting all subscriptions of %s that need a VAT Rate change") + subscriptions = stripe.Subscription.list(limit=100) # Increase the limit to 100 per page (maximum) + ch_subs = [] + + while subscriptions: + for subscription in subscriptions: + if len(subscription.default_tax_rates) > 0 and subscription.default_tax_rates[0].jurisdiction and subscription.default_tax_rates[0].jurisdiction.lower() == 'ch': + ch_subs.append(subscription) + elif len(subscription.default_tax_rates) > 0: + print("subscription %s belongs to %s" % (subscription.id, subscription.default_tax_rates[0].jurisdiction)) + else: + print("subscription %s does not have a tax rate" % subscription.id) + if subscriptions.has_more: + print("FETCHING MORE") + subscriptions = stripe.Subscription.list(limit=100, starting_after=subscriptions.data[-1]) + else: + break + logger.debug("There are %s ch subscription that need VAT rate update" % len(ch_subs)) + + # CSV column headers + csv_headers = [ + "customer_name", + "customer_email", + "stripe_customer_id", + "subscription_id", + "subscription_name", + "amount", + "vat_rate" + ] + # CSV file name + csv_filename = "ch_subscriptions_change_2024.csv" + # Write subscription data to CSV file + with open(csv_filename, mode='w', newline='') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=csv_headers) + writer.writeheader() + + for subscription in ch_subs: + subscription_id = subscription["id"] + stripe_customer_id = subscription.get("customer", "") + vat_rate = subscription.get("tax_percent", "") + c_user = CustomUser.objects.get( + id=StripeCustomer.objects.filter(stripe_id=stripe_customer_id)[0].user.id) + if c_user: + customer_name = c_user.name.encode('utf-8') + customer_email = c_user.email + items = subscription.get("items", {}).get("data", []) + for item in items: + subscription_name = item.get("plan", {}).get("id", "") + amount = item.get("plan", {}).get("amount", "") + + # Convert amount to a proper format (e.g., cents to dollars) + amount_in_chf = amount / 100 # Adjust this conversion as needed + + # Writing to CSV + writer.writerow({ + "customer_name": customer_name, + "customer_email": customer_email, + "stripe_customer_id": stripe_customer_id, + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "amount": amount_in_chf, + "vat_rate": vat_rate # Fill in VAT rate if available + }) + else: + print("No customuser for %s %s" % (stripe_customer_id, subscription_id)) + + + if MAKE_MODIFS: + print("Making modifications now") + tax_rate_obj = stripe.TaxRate.create( + display_name="VAT", + description="VAT for %s" % country_to_change, + jurisdiction=country_to_change, + percentage=new_rate, + inclusive=False, + ) + stripe_tax_rate = StripeTaxRate.objects.create( + display_name=tax_rate_obj.display_name, + description=tax_rate_obj.description, + jurisdiction=tax_rate_obj.jurisdiction, + percentage=tax_rate_obj.percentage, + inclusive=False, + tax_rate_id=tax_rate_obj.id + ) + + for ch_sub in ch_subs: + ch_sub.default_tax_rates = [stripe_tax_rate.tax_rate_id] + ch_sub.save() + logger.debug("Default tax rate updated for %s" % ch_sub.id) + else: + print("Not making any modifications because MAKE_MODIFS=False") + + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/hosting/management/commands/change_fi_vatrate_2024.py b/hosting/management/commands/change_fi_vatrate_2024.py new file mode 100644 index 00000000..6947b6eb --- /dev/null +++ b/hosting/management/commands/change_fi_vatrate_2024.py @@ -0,0 +1,143 @@ +from django.core.management.base import BaseCommand +import datetime +import csv +import logging +import stripe +from hosting.models import VATRates, StripeTaxRate +from utils.hosting_utils import get_vat_rate_for_country +from django.conf import settings +from membership.models import CustomUser, StripeCustomer + +stripe.api_key = settings.STRIPE_API_PRIVATE_KEY + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = '''FI vat rate changes on 2024-09-01 from 24% to 25.5%. This commands makes the necessary changes''' + + def handle(self, *args, **options): + MAKE_MODIFS=False + try: + country_to_change = 'FI' + currency_to_change = 'EUR' + new_rate = 25.5 + user_country_vat_rate = get_vat_rate_for_country(country_to_change) + logger.debug("Existing VATRate for %s %s " % (country_to_change, user_country_vat_rate)) + vat_rate = VATRates.objects.get( + territory_codes=country_to_change, start_date__isnull=False, stop_date=None + ) + logger.debug("VAT rate for %s is %s" % (country_to_change, vat_rate.rate)) + logger.debug("vat_rate object = %s" % vat_rate) + logger.debug("Create end date for the VATRate %s" % vat_rate.id) + #if MAKE_MODIFS: + # vat_rate.stop_date = datetime.date(2024, 8, 31) + # vat_rate.save() + # print("Creating a new VATRate for FI") + # obj, created = VATRates.objects.get_or_create( + # start_date=datetime.date(2024, 9, 1), + # stop_date=None, + # territory_codes=country_to_change, + # currency_code=currency_to_change, + # rate=new_rate * 0.01, + # rate_type="standard", + # description="FINLAND standard VAT (added manually on %s)" % datetime.datetime.now() + # ) + # if created: + # logger.debug("Created new VAT Rate for %s with the new rate %s" % (country_to_change, new_rate)) + # logger.debug(obj) + # else: + # logger.debug("VAT Rate for %s already exists with the rate %s" % (country_to_change, new_rate)) + logger.debug("Getting all subscriptions of %s that need a VAT Rate change") + subscriptions = stripe.Subscription.list(limit=100) # Increase the limit to 100 per page (maximum) + fi_subs = [] + + while subscriptions: + for subscription in subscriptions: + if len(subscription.default_tax_rates) > 0 and subscription.default_tax_rates[0].jurisdiction and subscription.default_tax_rates[0].jurisdiction.lower() == 'fi': + fi_subs.append(subscription) + elif len(subscription.default_tax_rates) > 0: + print("subscription %s belongs to %s" % (subscription.id, subscription.default_tax_rates[0].jurisdiction)) + else: + print("subscription %s does not have a tax rate" % subscription.id) + if subscriptions.has_more: + print("FETCHING MORE") + subscriptions = stripe.Subscription.list(limit=100, starting_after=subscriptions.data[-1]) + else: + break + logger.debug("There are %s FI subscription that need VAT rate update" % len(fi_subs)) + + # CSV column headers + csv_headers = [ + "customer_name", + "customer_email", + "stripe_customer_id", + "subscription_id", + "subscription_name", + "amount", + "vat_rate" + ] + # CSV file name + csv_filename = "fi_subscriptions_change_2024.csv" + # Write subscription data to CSV file + with open(csv_filename, mode='w', newline='') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=csv_headers) + writer.writeheader() + + for subscription in fi_subs: + subscription_id = subscription["id"] + stripe_customer_id = subscription.get("customer", "") + vat_rate = subscription.get("tax_percent", "") + c_user = CustomUser.objects.get( + id=StripeCustomer.objects.filter(stripe_id=stripe_customer_id)[0].user.id) + if c_user: + customer_name = c_user.name.encode('utf-8') + customer_email = c_user.email + items = subscription.get("items", {}).get("data", []) + for item in items: + subscription_name = item.get("plan", {}).get("id", "") + amount = item.get("plan", {}).get("amount", "") + + # Convert amount to a proper format (e.g., cents to dollars) + amount_in_chf = amount / 100 # Adjust this conversion as needed + + # Writing to CSV + writer.writerow({ + "customer_name": customer_name, + "customer_email": customer_email, + "stripe_customer_id": stripe_customer_id, + "subscription_id": subscription_id, + "subscription_name": subscription_name, + "amount": amount_in_chf, + "vat_rate": vat_rate # Fill in VAT rate if available + }) + else: + print("No customuser for %s %s" % (stripe_customer_id, subscription_id)) + + + if MAKE_MODIFS: + print("Making modifications now") + tax_rate_obj = stripe.TaxRate.create( + display_name="VAT", + description="VAT for %s" % country_to_change, + jurisdiction=country_to_change, + percentage=new_rate, + inclusive=False, + ) + stripe_tax_rate = StripeTaxRate.objects.create( + display_name=tax_rate_obj.display_name, + description=tax_rate_obj.description, + jurisdiction=tax_rate_obj.jurisdiction, + percentage=tax_rate_obj.percentage, + inclusive=False, + tax_rate_id=tax_rate_obj.id + ) + + for fi_sub in fi_subs: + fi_sub.default_tax_rates = [stripe_tax_rate.tax_rate_id] + fi_sub.save() + logger.debug("Default tax rate updated for %s" % fi_sub.id) + else: + print("Not making any modifications because MAKE_MODIFS=False") + + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) 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/migrations/0066_auto_20230727_0812.py b/hosting/migrations/0066_auto_20230727_0812.py new file mode 100644 index 00000000..795b1785 --- /dev/null +++ b/hosting/migrations/0066_auto_20230727_0812.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2023-07-27 08:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0065_auto_20201231_1041'), + ] + + operations = [ + migrations.AlterField( + model_name='genericproduct', + name='product_price', + field=models.DecimalField(decimal_places=2, max_digits=10), + ), + migrations.AlterField( + model_name='genericproduct', + name='product_vat', + field=models.DecimalField(decimal_places=4, default=0, max_digits=10), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 67c55aa2..12edba65 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,4 +1,3 @@ -import decimal import json import logging import os @@ -76,21 +75,28 @@ class GenericProduct(AssignPermissionsMixin, models.Model): ) product_description = models.CharField(max_length=500, default="") created_at = models.DateTimeField(auto_now_add=True) - product_price = models.DecimalField(max_digits=6, decimal_places=2) - product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0) + product_price = models.DecimalField(max_digits=10, decimal_places=2) + product_vat = models.DecimalField(max_digits=10, decimal_places=4, default=0) product_is_subscription = models.BooleanField(default=True) 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 @@ -685,15 +699,116 @@ class UserCardDetail(AssignPermissionsMixin, models.Model): @staticmethod def save_default_card_local(stripe_api_cus_id, card_id): + print("save_default_card_local {}, {}".format(stripe_api_cus_id, card_id)) stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id) - user_card_detail = UserCardDetail.objects.get( - stripe_customer=stripe_cust, card_id=card_id + print(" stripe_cust={}".format(stripe_cust)) + user_card_detail = UserCardDetail.get_ucd_from_stripe_cust_n_card_id( + stripe_cust, card_id ) + print(" user_card_detail={}".format(user_card_detail.__dict__)) for card in stripe_cust.usercarddetail_set.all(): card.preferred = False card.save() user_card_detail.preferred = True user_card_detail.save() + print(" save_default_card_local DONE") + + @staticmethod + def get_ucd_from_card_id(card_id): + try: + user_card_details = UserCardDetail.objects.filter( + card_id=card_id + ).order_by('-id') + + if user_card_details.count() > 1: + # Log a warning about the duplicate entries + logger.warning( + "Multiple UserCardDetail objects found for card_id={}. " + "Found {} objects. Using the latest one.".format( + card_id, user_card_details.count() + ) + ) + # Use the first object found + user_card_detail = user_card_details.first() + elif user_card_details.count() == 1: + # Exactly one object found, proceed as intended + user_card_detail = user_card_details.first() + else: + # No object found for the given customer and card_id. + # Depending on expected behavior, you might want to raise an error or handle this case. + # If the original get() call happened here, it would raise DoesNotExist. + logger.error( + "No UserCardDetail found for card_id={}.".format(card_id) + ) + raise UserCardDetail.DoesNotExist("No UserCardDetail found for card {}".format(card_id)) + if user_card_details.count() > 1: + # Log a warning about the duplicate entries + logger.warning( + "Multiple UserCardDetail objects found for card_id={}. " + "Found {} objects. Using the first one.".format( + card_id, user_card_details.count() + ) + ) + # Use the first object found + user_card_detail = user_card_details.first() + elif user_card_details.count() == 1: + # Exactly one object found, proceed as intended + user_card_detail = user_card_details.first() + else: + # No object found for the given customer and card_id. + # Depending on expected behavior, you might want to raise an error or handle this case. + # If the original get() call happened here, it would raise DoesNotExist. + logger.error( + "No UserCardDetail found for card_id={}.".format(card_id) + ) + raise UserCardDetail.DoesNotExist("No UserCardDetail found for card {}".format(card_id)) + except Exception as e: + # Catch other potential exceptions during the filter/get process if necessary + logger.error("An unexpected error occurred while fetching UserCardDetail: {}".format(e)) + raise + return user_card_detail + + @staticmethod + def get_ucd_from_stripe_cust_n_card_id(stripe_cust, card_id): + try: + user_card_details = UserCardDetail.objects.filter( + stripe_customer=stripe_cust, card_id=card_id + ).order_by('-id') + + if user_card_details.count() > 1: + # Log a warning about the duplicate entries + logger.warning( + "Multiple UserCardDetail objects found for stripe_customer_id={} and card_id={}. " + "Found {} objects. Using the first one.".format( + stripe_cust.id, card_id, user_card_details.count() + ) + ) + # Use the first object found + user_card_detail = user_card_details.first() + elif user_card_details.count() == 1: + # Exactly one object found, proceed as intended + user_card_detail = user_card_details.first() + else: + # No object found for the given customer and card_id. + # Depending on expected behavior, you might want to raise an error or handle this case. + # If the original get() call happened here, it would raise DoesNotExist. + logger.error( + "No UserCardDetail found for stripe_customer_id={} and card_id={}.".format( + stripe_cust.id, card_id + ) + ) + raise UserCardDetail.DoesNotExist( + "No UserCardDetail found for customer {}, card {}".format( + stripe_cust.id, card_id + ) + ) + + except Exception as e: + # Catch other potential exceptions during the filter/get process if necessary + logger.error("An unexpected error occurred while fetching UserCardDetail: {}".format(e)) + raise + return user_card_detail + @staticmethod def get_user_card_details(stripe_customer, card_details): @@ -734,3 +849,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 %}.
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 8dd01906..c2470707 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 ) @@ -552,20 +554,31 @@ class SettingsView(LoginRequiredMixin, FormView): Check if the user already saved contact details. If so, then show the form populated with those details, to let user change them. """ + username = self.request.GET.get('username') + if self.request.user.is_admin and username: + user = CustomUser.objects.get(username=username) + else: + user = self.request.user return form_class( - instance=self.request.user.billing_addresses.first(), + instance=user.billing_addresses.first(), **self.get_form_kwargs()) def get_context_data(self, **kwargs): context = super(SettingsView, self).get_context_data(**kwargs) # Get user - user = self.request.user + username = self.request.GET.get('username') + if self.request.user.is_admin and username: + user = CustomUser.objects.get(username=username) + else: + user = self.request.user 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 +589,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 +695,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 @@ -809,7 +810,7 @@ class PaymentVMView(LoginRequiredMixin, FormView): card_id = form.cleaned_data.get('card') customer = owner.stripecustomer try: - user_card_detail = UserCardDetail.objects.get(id=card_id) + user_card_detail = UserCardDetail.get_ucd_from_card_id(card_id=card_id) if not request.user.has_perm( 'view_usercarddetail', user_card_detail ): @@ -1013,7 +1014,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): context['cc_exp_month'] = card_details_response['exp_month'] else: card_id = self.request.session.get('card_id') - card_detail = UserCardDetail.objects.get(id=card_id) + card_detail = UserCardDetail.get_ucd_from_card_id(card_id=card_id) context['cc_last4'] = card_detail.last4 context['cc_brand'] = card_detail.brand context['cc_exp_year'] = card_detail.exp_year @@ -1077,7 +1078,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 +1101,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: @@ -1121,7 +1128,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): return JsonResponse(response) else: card_id = request.session.get('card_id') - user_card_detail = UserCardDetail.objects.get(id=card_id) + user_card_detail = UserCardDetail.get_ucd_from_card_id(card_id=card_id) card_details_dict = { 'last4': user_card_detail.last4, 'brand': user_card_detail.brand, @@ -1185,10 +1192,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'): @@ -1282,10 +1310,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) ) @@ -1295,7 +1324,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) @@ -1303,6 +1333,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: 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( @@ -1316,10 +1361,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) @@ -1482,7 +1544,12 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): ordering = '-id' def get_queryset(self): - owner = self.request.user + username = self.request.GET.get('username') + if self.request.user.is_admin and username: + user = CustomUser.objects.get(username=username) + else: + user = self.request.user + owner = user manager = OpenNebulaManager(email=owner.username, password=owner.password) try: @@ -1641,7 +1708,11 @@ class VirtualMachineView(LoginRequiredMixin, View): login_url = reverse_lazy('hosting:login') def get_object(self): - owner = self.request.user + username = self.request.GET.get('username') + if self.request.user.is_admin and username: + owner = CustomUser.objects.get(username=username) + else: + owner = self.request.user vm = None manager = OpenNebulaManager( email=owner.username, @@ -1697,7 +1768,10 @@ class VirtualMachineView(LoginRequiredMixin, View): subscription=hosting_order.subscription_id, count=1 ) - inv_url = stripe_obj.data[0].hosted_invoice_url + if stripe_obj.data: + inv_url = stripe_obj.data[0].hosted_invoice_url + else: + inv_url = '' elif hosting_order.stripe_charge_id: stripe_obj = stripe.Charge.retrieve( hosting_order.stripe_charge_id @@ -1778,7 +1852,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: @@ -1801,6 +1875,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. " @@ -1854,7 +1932,7 @@ class VirtualMachineView(LoginRequiredMixin, View): '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()]), } diff --git a/membership/models.py b/membership/models.py index 703b4800..81054fb9 100644 --- a/membership/models.py +++ b/membership/models.py @@ -277,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 @@ -288,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 @@ -296,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/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..73cdf987 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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/ungleich_page/templates/ungleich_page/ungleich/section_products.html b/ungleich_page/templates/ungleich_page/ungleich/section_products.html index 9cdc94c8..e3d0dc73 100644 --- a/ungleich_page/templates/ungleich_page/ungleich/section_products.html +++ b/ungleich_page/templates/ungleich_page/ungleich/section_products.html @@ -19,13 +19,15 @@ \ No newline at end of file + diff --git a/ungleich_page/views.py b/ungleich_page/views.py index e5a99d8d..01d1138d 100644 --- a/ungleich_page/views.py +++ b/ungleich_page/views.py @@ -25,9 +25,10 @@ class ContactView(FormView): success_message = _('Message Successfully Sent') def form_valid(self, form): - form.save() - form.send_email() - messages.add_message(self.request, messages.SUCCESS, self.success_message) + print("ungleich_page contactusform") + #form.save() + #form.send_email() + #messages.add_message(self.request, messages.SUCCESS, self.success_message) return super(ContactView, self).form_valid(form) def get_context_data(self, **kwargs): diff --git a/utils/fields.py b/utils/fields.py index 48a606cc..ca1115de 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -1,7 +1,8 @@ from django.utils.translation import ugettext as _ from django.db import models -# http://xml.coverpages.org/country3166.html +# Old: http://xml.coverpages.org/country3166.html +# 2023-12-29: Updated list of countries from https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes COUNTRIES = ( ('AD', _('Andorra')), ('AE', _('United Arab Emirates')), @@ -10,7 +11,6 @@ COUNTRIES = ( ('AI', _('Anguilla')), ('AL', _('Albania')), ('AM', _('Armenia')), - ('AN', _('Netherlands Antilles')), ('AO', _('Angola')), ('AQ', _('Antarctica')), ('AR', _('Argentina')), @@ -18,6 +18,7 @@ COUNTRIES = ( ('AT', _('Austria')), ('AU', _('Australia')), ('AW', _('Aruba')), + ('AX', _('Aland Islands')), ('AZ', _('Azerbaijan')), ('BA', _('Bosnia and Herzegovina')), ('BB', _('Barbados')), @@ -28,11 +29,13 @@ COUNTRIES = ( ('BH', _('Bahrain')), ('BI', _('Burundi')), ('BJ', _('Benin')), + ('BL', _('St. Barts')), ('BM', _('Bermuda')), - ('BN', _('Brunei Darussalam')), + ('BN', _('Brunei')), ('BO', _('Bolivia')), + ('BQ', _('Caribbean Netherlands')), ('BR', _('Brazil')), - ('BS', _('Bahama')), + ('BS', _('Bahamas')), ('BT', _('Bhutan')), ('BV', _('Bouvet Island')), ('BW', _('Botswana')), @@ -40,11 +43,12 @@ COUNTRIES = ( ('BZ', _('Belize')), ('CA', _('Canada')), ('CC', _('Cocos (Keeling) Islands')), + ('CD', _('Congo - Kinshasa')), ('CF', _('Central African Republic')), - ('CG', _('Congo')), + ('CG', _('Congo - Brazzaville')), ('CH', _('Switzerland')), ('CI', _('Ivory Coast')), - ('CK', _('Cook Iislands')), + ('CK', _('Cook Islands')), ('CL', _('Chile')), ('CM', _('Cameroon')), ('CN', _('China')), @@ -52,9 +56,10 @@ COUNTRIES = ( ('CR', _('Costa Rica')), ('CU', _('Cuba')), ('CV', _('Cape Verde')), + ('CW', _('Curacao')), ('CX', _('Christmas Island')), ('CY', _('Cyprus')), - ('CZ', _('Czech Republic')), + ('CZ', _('Czechia')), ('DE', _('Germany')), ('DJ', _('Djibouti')), ('DK', _('Denmark')), @@ -70,16 +75,16 @@ COUNTRIES = ( ('ET', _('Ethiopia')), ('FI', _('Finland')), ('FJ', _('Fiji')), - ('FK', _('Falkland Islands (Malvinas)')), + ('FK', _('Falkland Islands')), ('FM', _('Micronesia')), ('FO', _('Faroe Islands')), ('FR', _('France')), - ('FX', _('France, Metropolitan')), ('GA', _('Gabon')), - ('GB', _('United Kingdom (Great Britain)')), + ('GB', _('United Kingdom')), ('GD', _('Grenada')), ('GE', _('Georgia')), ('GF', _('French Guiana')), + ('GG', _('Guernsey')), ('GH', _('Ghana')), ('GI', _('Gibraltar')), ('GL', _('Greenland')), @@ -93,7 +98,7 @@ COUNTRIES = ( ('GU', _('Guam')), ('GW', _('Guinea-Bissau')), ('GY', _('Guyana')), - ('HK', _('Hong Kong')), + ('HK', _('Hong Kong SAR China')), ('HM', _('Heard & McDonald Islands')), ('HN', _('Honduras')), ('HR', _('Croatia')), @@ -102,12 +107,14 @@ COUNTRIES = ( ('ID', _('Indonesia')), ('IE', _('Ireland')), ('IL', _('Israel')), + ('IM', _('Isle of Man')), ('IN', _('India')), ('IO', _('British Indian Ocean Territory')), ('IQ', _('Iraq')), - ('IR', _('Islamic Republic of Iran')), + ('IR', _('Iran')), ('IS', _('Iceland')), ('IT', _('Italy')), + ('JE', _('Jersey')), ('JM', _('Jamaica')), ('JO', _('Jordan')), ('JP', _('Japan')), @@ -117,14 +124,14 @@ COUNTRIES = ( ('KI', _('Kiribati')), ('KM', _('Comoros')), ('KN', _('St. Kitts and Nevis')), - ('KP', _('Korea, Democratic People\'s Republic of')), - ('KR', _('Korea, Republic of')), + ('KP', _('North Korea')), + ('KR', _('South Korea')), ('KW', _('Kuwait')), ('KY', _('Cayman Islands')), ('KZ', _('Kazakhstan')), - ('LA', _('Lao People\'s Democratic Republic')), + ('LA', _('Laos')), ('LB', _('Lebanon')), - ('LC', _('Saint Lucia')), + ('LC', _('St. Lucia')), ('LI', _('Liechtenstein')), ('LK', _('Sri Lanka')), ('LR', _('Liberia')), @@ -132,20 +139,23 @@ COUNTRIES = ( ('LT', _('Lithuania')), ('LU', _('Luxembourg')), ('LV', _('Latvia')), - ('LY', _('Libyan Arab Jamahiriya')), + ('LY', _('Libya')), ('MA', _('Morocco')), ('MC', _('Monaco')), - ('MD', _('Moldova, Republic of')), + ('MD', _('Moldova')), + ('ME', _('Montenegro')), + ('MF', _('St. Martin')), ('MG', _('Madagascar')), ('MH', _('Marshall Islands')), + ('MK', _('North Macedonia')), ('ML', _('Mali')), + ('MM', _('Myanmar (Burma)')), ('MN', _('Mongolia')), - ('MM', _('Myanmar')), - ('MO', _('Macau')), + ('MO', _('Macao SAR China')), ('MP', _('Northern Mariana Islands')), ('MQ', _('Martinique')), ('MR', _('Mauritania')), - ('MS', _('Monserrat')), + ('MS', _('Montserrat')), ('MT', _('Malta')), ('MU', _('Mauritius')), ('MV', _('Maldives')), @@ -174,15 +184,17 @@ COUNTRIES = ( ('PK', _('Pakistan')), ('PL', _('Poland')), ('PM', _('St. Pierre & Miquelon')), - ('PN', _('Pitcairn')), + ('PN', _('Pitcairn Islands')), ('PR', _('Puerto Rico')), + ('PS', _('Palestinian Territories')), ('PT', _('Portugal')), ('PW', _('Palau')), ('PY', _('Paraguay')), ('QA', _('Qatar')), ('RE', _('Reunion')), ('RO', _('Romania')), - ('RU', _('Russian Federation')), + ('RS', _('Serbia')), + ('RU', _('Russia')), ('RW', _('Rwanda')), ('SA', _('Saudi Arabia')), ('SB', _('Solomon Islands')), @@ -192,17 +204,19 @@ COUNTRIES = ( ('SG', _('Singapore')), ('SH', _('St. Helena')), ('SI', _('Slovenia')), - ('SJ', _('Svalbard & Jan Mayen Islands')), + ('SJ', _('Svalbard and Jan Mayen')), ('SK', _('Slovakia')), ('SL', _('Sierra Leone')), ('SM', _('San Marino')), ('SN', _('Senegal')), ('SO', _('Somalia')), ('SR', _('Suriname')), + ('SS', _('South Sudan')), ('ST', _('Sao Tome & Principe')), ('SV', _('El Salvador')), - ('SY', _('Syrian Arab Republic')), - ('SZ', _('Swaziland')), + ('SX', _('Sint Maarten')), + ('SY', _('Syria')), + ('SZ', _('Eswatini')), ('TC', _('Turks & Caicos Islands')), ('TD', _('Chad')), ('TF', _('French Southern Territories')), @@ -210,36 +224,34 @@ COUNTRIES = ( ('TH', _('Thailand')), ('TJ', _('Tajikistan')), ('TK', _('Tokelau')), + ('TL', _('Timor-Leste')), ('TM', _('Turkmenistan')), ('TN', _('Tunisia')), ('TO', _('Tonga')), - ('TP', _('East Timor')), ('TR', _('Turkey')), ('TT', _('Trinidad & Tobago')), ('TV', _('Tuvalu')), - ('TW', _('Taiwan, Province of China')), - ('TZ', _('Tanzania, United Republic of')), + ('TW', _('Taiwan')), + ('TZ', _('Tanzania')), ('UA', _('Ukraine')), ('UG', _('Uganda')), - ('UM', _('United States Minor Outlying Islands')), - ('US', _('United States of America')), + ('UM', _('U.S. Outlying Islands')), + ('US', _('United States')), ('UY', _('Uruguay')), ('UZ', _('Uzbekistan')), - ('VA', _('Vatican City State (Holy See)')), - ('VC', _('St. Vincent & the Grenadines')), + ('VA', _('Vatican City')), + ('VC', _('St. Vincent & Grenadines')), ('VE', _('Venezuela')), ('VG', _('British Virgin Islands')), - ('VI', _('United States Virgin Islands')), - ('VN', _('Viet Nam')), + ('VI', _('U.S. Virgin Islands')), + ('VN', _('Vietnam')), ('VU', _('Vanuatu')), - ('WF', _('Wallis & Futuna Islands')), + ('WF', _('Wallis & Futuna')), ('WS', _('Samoa')), ('YE', _('Yemen')), ('YT', _('Mayotte')), - ('YU', _('Yugoslavia')), ('ZA', _('South Africa')), ('ZM', _('Zambia')), - ('ZR', _('Zaire')), ('ZW', _('Zimbabwe')), ) diff --git a/utils/forms.py b/utils/forms.py index f35c90f4..3cc57578 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -4,6 +4,8 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ +from django_recaptcha.fields import ReCaptchaField + from membership.models import CustomUser from .models import ContactMessage, BillingAddress, UserBillingAddress @@ -188,6 +190,7 @@ class UserBillingAddressForm(forms.ModelForm): class ContactUsForm(forms.ModelForm): error_css_class = 'autofocus' + captcha = ReCaptchaField() class Meta: model = ContactMessage @@ -206,11 +209,12 @@ class ContactUsForm(forms.ModelForm): } def send_email(self, email_to='info@digitalglarus.ch'): - text_content = render_to_string( - 'emails/contact.txt', {'data': self.cleaned_data}) - html_content = render_to_string( - 'emails/contact.html', {'data': self.cleaned_data}) - email = EmailMultiAlternatives('Subject', text_content) - email.attach_alternative(html_content, "text/html") - email.to = [email_to] - email.send() + pass + #text_content = render_to_string( + # 'emails/contact.txt', {'data': self.cleaned_data}) + #html_content = render_to_string( + # 'emails/contact.html', {'data': self.cleaned_data}) + #email = EmailMultiAlternatives('Subject', text_content) + #email.attach_alternative(html_content, "text/html") + #email.to = [email_to] + #email.send() 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 8c555224..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 @@ -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/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)