diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..6b8710a7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.git
diff --git a/Changelog b/Changelog
index 2c2d73d7..44c4dee4 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,53 @@
+3.2: 2021-02-07
+ * 8816: Update order confirmation text to better prepared for payment dispute
+ * supportticket#22990: Fix: can't add a deleted card
+3.1: 2021-01-11
+ * 8781: Fix error is setting a default card (MR!746)
+3.0: 2021-01-07
+ * 8393: Implement SCA for stripe payments (MR!745)
+ * 8691: Implment check_vm_templates management command (MR!744)
+2.14: 2020-12-07
+ * 8692: Create a script that fixes django db for the order after celery error (MR!743)
+2.13: 2020-12-02
+ * 8654: Fix 500 error on invoices list for the user contact+devuanhosting.com@virus.media (MR!742)
+ * 8593: Escape user's ssh key in xml-rpc call to create VM (MR!741)
+2.12.1: 2020-07-21
+ * 8307: Introduce "Exclude vat calculations" for Generic Products (MR!740)
+ * Change DE VAT rate to 16% from 19% (MR!739)
+2.12: 2020-06-23
+ * 7894: Show one time payment invoices (MR!738)
+2.11: 2020-06-11
+ * Bugfix: Correct the wrong constant name (caused payment to go thru and showing error and VMs not instantiated)
+2.10.8: 2020-06-10
+ * #8102: Refactor MAX_TIME_TO_WAIT_FOR_VM_TERMINATE to increase time to poll whether VM has been terminated or not (MR!737)
+2.10.7: 2020-05-25
+ * Bugfix: Handle VM templates deleted in OpenNebula but VM instances still existing (MR!736)
+ Notes for deployment:
+ When deploying define a UPDATED_TEMPLATES string represented dictionary value in .env
+```
+ # Represents Template Ids that were
+ # deleted and the new template Id to look for the template
+ # definition
+ UPDATED_TEMPLATES="{1: 100}"
+```
+2.10.6: 2020-03-25
+ * Bugfix: Handle Nonetype for discount's name (MR!735)
+2.10.5: 2020-03-17
+ * Introduce base price for VMs and let admins add stripe_coupon_id (MR!730)
+ Notes for deployment:
+ 1. Add env variable `VM_BASE_PRICE`
+ 2. Migrate datacenterlight app. This introduces the stripe_coupon_code field in the VMPricing.
+ 3. Create a coupon in stripe with the desired value and note down the stripe's coupon id
+ 4. Update the discount amount and set the corresponding coupon id in the admin
+2.10.3b: 2020-03-05
+ * #7773: Use username for communicating with opennebula all the time
+2.10.2b: 2020-02-25
+ * #7764: Fix uid represented as bytestring
+ * #7769: [hosting] ssh private key download feature does not work well on Firefox
+2.10.1: 2020-02-02:
+ * Changes the pricing structure of generic products into the pre vat and with vat (like that for VM)
+ * Shows product name (if exists) in the invoices list if it belongs to a generic product
+ * Small bugfixes (right alignment of price in the invoice list, show prices with 2 decimal places etc)
2.10: 2020-02-01
* Feature: Introduce new design to show VAT exclusive/VAT inclusive pricing together
* Feature: Separate VAT and discount in Stripe
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..50b81cbb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM python:3.10.0-alpine3.15
+
+WORKDIR /usr/src/app
+
+RUN apk add --update --no-cache \
+ git \
+ build-base \
+ openldap-dev \
+ python3-dev \
+ libpq-dev \
+ && rm -rf /var/cache/apk/*
+
+# FIX https://github.com/python-ldap/python-ldap/issues/432
+RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so
+
+COPY requirements.txt ./
+RUN pip install --no-cache-dir -r requirements.txt
+COPY ./ .
diff --git a/Makefile b/Makefile
index 67c0c15b..68ff014e 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,12 @@ help:
@echo ' make rsync_upload '
@echo ' make install_debian_packages '
+buildimage:
+ docker build -t dynamicweb:$$(git describe) .
+
+releaseimage: buildimage
+ ./release.sh
+
collectstatic:
$(PY?) $(BASEDIR)/manage.py collectstatic
diff --git a/datacenterlight/cms_plugins.py b/datacenterlight/cms_plugins.py
index c3ec974f..52b4f19f 100644
--- a/datacenterlight/cms_plugins.py
+++ b/datacenterlight/cms_plugins.py
@@ -1,5 +1,6 @@
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
+from django.conf import settings
from .cms_models import (
DCLBannerItemPluginModel, DCLBannerListPluginModel, DCLContactPluginModel,
@@ -100,6 +101,7 @@ class DCLCalculatorPlugin(CMSPluginBase):
vm_type=instance.vm_type
).order_by('name')
context['instance'] = instance
+ context['vm_base_price'] = settings.VM_BASE_PRICE
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
return context
diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po
index 1a1a2a26..cd7fab99 100644
--- a/datacenterlight/locale/de/LC_MESSAGES/django.po
+++ b/datacenterlight/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-02-01 09:42+0000\n"
+"POT-Creation-Date: 2021-02-07 11:10+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User '\n"
"Language-Team: LANGUAGE \n"
@@ -144,8 +144,8 @@ msgid ""
"the heart of Switzerland."
msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz."
-msgid "Try now, order a VM. VM price starts from only 10.5 CHF per month."
-msgstr "Unser Angebot beginnt bei 10.5 CHF pro Monat. Probier's jetzt aus!"
+msgid "Try now, order a VM. VM price starts from only 11.5 CHF per month."
+msgstr "Unser Angebot beginnt bei 11.5 CHF pro Monat. Probier's jetzt aus!"
msgid "ORDER VM"
msgstr "VM BESTELLEN"
@@ -415,8 +415,9 @@ msgstr "Deine MwSt-Nummer wurde überprüft"
msgid ""
"Your VAT number is under validation. VAT will be adjusted, once the "
"validation is complete."
-msgstr "Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, "
-"sobald die Validierung abgeschlossen ist."
+msgstr ""
+"Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, sobald "
+"die Validierung abgeschlossen ist."
msgid "Payment method"
msgstr "Bezahlmethode"
@@ -430,18 +431,6 @@ msgstr "Bestellungsübersicht"
msgid "Product"
msgstr "Produkt"
-msgid "Price"
-msgstr "Preise"
-
-msgid "VAT for"
-msgstr "MwSt für"
-
-msgid "Total Amount"
-msgstr "Gesamtsumme"
-
-msgid "Amount"
-msgstr "Betrag"
-
msgid "Description"
msgstr "Beschreibung"
@@ -454,42 +443,51 @@ msgstr "Preis ohne MwSt."
msgid "Pre VAT"
msgstr "Exkl. MwSt."
+msgid "VAT for"
+msgstr "MwSt für"
+
msgid "Your Price in Total"
msgstr "Dein Gesamtpreis"
+#, python-format
msgid ""
-"By clicking \"Place order\" this plan will charge your credit card account "
-"with %(total_price)s CHF/year"
+" By clicking \"Place order\" you agree to our Terms of Service and "
+"this plan will charge your credit card account with %(total_price)s CHF/year"
msgstr ""
-"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s "
-"CHF pro Jahr belastet"
+"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Jahr belastet."
#, python-format
msgid ""
-"By clicking \"Place order\" this plan will charge your credit card account "
-"with %(total_price)s CHF/month"
+"\n"
+" By clicking \"Place order\" you agree to "
+"our Terms "
+"of Service and this plan will charge your credit card account with "
+"%(total_price)s CHF/month"
msgstr ""
-"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s "
-"CHF pro Monat belastet"
-
-#, fuzzy, python-format
-#| msgid ""
-#| "By clicking \"Place order\" this payment will charge your credit card "
-#| "account with a one time amount of %(total_price)s CHF"
-msgid ""
-"By clicking \"Place order\" this payment will charge your credit card "
-"account with a one time amount of %(total_price)s CHF"
-msgstr ""
-"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
-"%(vm_total_price)s CHF pro Monat belastet"
+"\n"
+"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Monat belastet."
#, python-format
msgid ""
-"By clicking \"Place order\" this plan will charge your credit card account "
-"with %(vm_total_price)s CHF/month"
+"By clicking \"Place order\" you agree to our Terms of Service and "
+"this plan will charge your credit card account with %(total_price)s CHF"
msgstr ""
-"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
-"%(vm_total_price)s CHF pro Monat belastet"
+"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF belastet."
+
+#, python-format
+msgid ""
+"By clicking \"Place order\" you agree to our Terms of Service and "
+"this plan will charge your credit card account with %(vm_total_price)s CHF/"
+"month"
+msgstr ""
+"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren Nutzungsbedingungen einverstanden und Dein Kreditkartenkonto wird mit %(vm_total_price)s CHF/Monat belastet"
msgid "Place order"
msgstr "Bestellen"
@@ -608,16 +606,22 @@ msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
-#, python-brace-format
-msgid "{user} does not have permission to access the card"
-msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
-
-msgid "An error occurred. Details: {}"
-msgstr "Ein Fehler ist aufgetreten. Details: {}"
-
msgid "Confirm Order"
msgstr "Bestellung Bestätigen"
+#, fuzzy
+#| msgid "Thank you!"
+msgid "Thank you !"
+msgstr "Vielen Dank!"
+
+msgid "Your product will be provisioned as soon as we receive the payment."
+msgstr ""
+
+#, python-brace-format
+msgid "An error occurred while associating the card. Details: {details}"
+msgstr ""
+"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
+
msgid "Error."
msgstr ""
@@ -628,10 +632,21 @@ msgstr ""
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
"Popup zur Bezahlseite weitergeleitet."
-#, python-brace-format
-msgid "An error occurred while associating the card. Details: {details}"
+msgid "Thank you for the order."
+msgstr "Danke für Deine Bestellung."
+
+msgid ""
+"Your product will be provisioned as soon as we receive a payment "
+"confirmation from Stripe. We will send you a confirmation email. You can "
+"always contact us at support@datacenterlight.ch"
msgstr ""
-"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
+
+msgid ""
+"Your VM will be up and running in a few moments. We will send you a "
+"confirmation email as soon as it is ready."
+msgstr ""
+"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
+"auf sie zugreifen kannst."
msgid " This is a monthly recurring plan."
msgstr "Dies ist ein monatlich wiederkehrender Plan."
@@ -671,15 +686,40 @@ msgstr ""
"Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst "
"jederzeit unter info@ungleich.ch kontaktieren."
-msgid "Thank you for the order."
-msgstr "Danke für Deine Bestellung."
+#, python-format
+#~ msgid ""
+#~ "By clicking \"Place order\" this plan will charge your credit card "
+#~ "account with %(total_price)s CHF/month"
+#~ msgstr ""
+#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
+#~ "%(total_price)s CHF pro Monat belastet"
-msgid ""
-"Your VM will be up and running in a few moments. We will send you a "
-"confirmation email as soon as it is ready."
-msgstr ""
-"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
-"auf sie zugreifen kannst."
+#, fuzzy, python-format
+#~| msgid ""
+#~| "By clicking \"Place order\" this payment will charge your credit card "
+#~| "account with a one time amount of %(total_price)s CHF"
+#~ msgid ""
+#~ "By clicking \"Place order\" this payment will charge your credit card "
+#~ "account with a one time amount of %(total_price)s CHF"
+#~ msgstr ""
+#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
+#~ "%(vm_total_price)s CHF pro Monat belastet"
+
+#, python-brace-format
+#~ msgid "{user} does not have permission to access the card"
+#~ msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
+
+#~ msgid "An error occurred. Details: {}"
+#~ msgstr "Ein Fehler ist aufgetreten. Details: {}"
+
+#~ msgid "Price"
+#~ msgstr "Preise"
+
+#~ msgid "Total Amount"
+#~ msgstr "Gesamtsumme"
+
+#~ msgid "Amount"
+#~ msgstr "Betrag"
#~ msgid "Subtotal"
#~ msgstr "Zwischensumme"
@@ -746,9 +786,6 @@ msgstr ""
#~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie "
#~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld."
-#~ msgid "Thank you!"
-#~ msgstr "Vielen Dank!"
-
#~ msgid "Thank you for order! Our team will contact you via email"
#~ msgstr ""
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich "
diff --git a/datacenterlight/management/commands/check_vm_templates.py b/datacenterlight/management/commands/check_vm_templates.py
new file mode 100644
index 00000000..db36fde8
--- /dev/null
+++ b/datacenterlight/management/commands/check_vm_templates.py
@@ -0,0 +1,65 @@
+from django.core.management.base import BaseCommand
+from opennebula_api.models import OpenNebulaManager
+from datacenterlight.models import VMTemplate
+from membership.models import CustomUser
+
+from django.conf import settings
+from time import sleep
+import datetime
+import json
+import logging
+import os
+
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ help = '''Checks all VM templates to find if they can be instantiated'''
+
+ def add_arguments(self, parser):
+ parser.add_argument('user_email', type=str)
+
+ def handle(self, *args, **options):
+ result_dict = {}
+ user_email = options['user_email'] if 'user_email' in options else ""
+
+ if user_email:
+ cu = CustomUser.objects.get(email=user_email)
+ specs = {'cpu': 1, 'memory': 1, 'disk_size': 10}
+ manager = OpenNebulaManager(email=user_email, password=cu.password)
+ pub_keys = [settings.TEST_MANAGE_SSH_KEY_PUBKEY]
+ PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
+ if not os.path.exists("%s/outputs" % PROJECT_PATH):
+ os.mkdir("%s/outputs" % PROJECT_PATH)
+ for vm_template in VMTemplate.objects.all():
+ vm_name = 'test-%s' % vm_template.name
+ vm_id = manager.create_vm(
+ template_id=vm_template.opennebula_vm_template_id,
+ specs=specs,
+ ssh_key='\n'.join(pub_keys),
+ vm_name=vm_name
+ )
+ if vm_id and vm_id > 0:
+ result_dict[vm_name] = "%s OK, created VM %s" % (
+ '%s %s %s' % (vm_template.opennebula_vm_template_id,
+ vm_template.name, vm_template.vm_type),
+ vm_id
+ )
+ self.stdout.write(self.style.SUCCESS(result_dict[vm_name]))
+ manager.delete_vm(vm_id)
+ else:
+ result_dict[vm_name] = '''Error creating VM %s, template_id
+ %s %s''' % (vm_name,
+ vm_template.opennebula_vm_template_id,
+ vm_template.vm_type)
+ self.stdout.write(self.style.ERROR(result_dict[vm_name]))
+ sleep(1)
+ date_str = datetime.datetime.strftime(
+ datetime.datetime.now(), '%Y%m%d%H%M%S'
+ )
+ with open("%s/outputs/check_vm_templates_%s.txt" %
+ (PROJECT_PATH, date_str),
+ 'w',
+ encoding='utf-8') as f:
+ f.write(json.dumps(result_dict))
+ self.stdout.write(self.style.SUCCESS("Done"))
diff --git a/datacenterlight/management/commands/fix_vm_after_celery_error.py b/datacenterlight/management/commands/fix_vm_after_celery_error.py
new file mode 100644
index 00000000..0cfdb423
--- /dev/null
+++ b/datacenterlight/management/commands/fix_vm_after_celery_error.py
@@ -0,0 +1,76 @@
+from django.core.management.base import BaseCommand
+from datacenterlight.tasks import handle_metadata_and_emails
+from opennebula_api.models import OpenNebulaManager
+from membership.models import CustomUser
+import logging
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ help = '''Updates the DB after manual creation of VM'''
+
+ def add_arguments(self, parser):
+ parser.add_argument('vm_id', type=int)
+ parser.add_argument('order_id', type=int)
+ parser.add_argument('user', type=str)
+ parser.add_argument('specs', type=str)
+ parser.add_argument('template', type=str)
+
+ def handle(self, *args, **options):
+ vm_id = options['vm_id']
+ order_id = options['order_id']
+ user_str = options['user']
+ specs_str = options['specs']
+ template_str = options['template']
+
+ json_acceptable_string = user_str.replace("'", "\"")
+ user_dict = json.loads(json_acceptable_string)
+
+ json_acceptable_string = specs_str.replace("'", "\"")
+ specs = json.loads(json_acceptable_string)
+
+ json_acceptable_string = template_str.replace("'", "\"")
+ template = json.loads(json_acceptable_string)
+ if vm_id <= 0:
+ self.stdout.write(self.style.ERROR(
+ 'vm_id can\'t be less than or 0. Given: %s' % vm_id))
+ return
+ if vm_id <= 0:
+ self.stdout.write(self.style.ERROR(
+ 'order_id can\'t be less than or 0. Given: %s' % vm_id))
+ return
+ if specs_str is None or specs_str == "":
+ self.stdout.write(
+ self.style.ERROR('specs can\'t be empty or None'))
+ return
+
+ user = {
+ 'name': user_dict['name'],
+ 'email': user_dict['email'],
+ 'username': user_dict['username'],
+ 'pass': user_dict['pass'],
+ 'request_scheme': user_dict['request_scheme'],
+ 'request_host': user_dict['request_host'],
+ 'language': user_dict['language'],
+ }
+ cu = CustomUser.objects.get(username=user.get('username'))
+ # Create OpenNebulaManager
+ self.stdout.write(
+ self.style.SUCCESS(
+ 'Connecting using %s' % (cu.username)
+ )
+ )
+ manager = OpenNebulaManager(email=cu.username, password=cu.password)
+ handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
+ template)
+ self.stdout.write(
+ self.style.SUCCESS(
+ 'Done handling metadata and emails for %s %s %s' % (
+ order_id,
+ vm_id,
+ str(user)
+ )
+ )
+ )
diff --git a/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py b/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py
new file mode 100644
index 00000000..d2e45871
--- /dev/null
+++ b/datacenterlight/migrations/0031_vmpricing_stripe_coupon_id.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2020-02-04 03:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('datacenterlight', '0030_dclnavbarpluginmodel_show_non_transparent_navbar_always'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vmpricing',
+ name='stripe_coupon_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/datacenterlight/models.py b/datacenterlight/models.py
index 6410254b..64d785a2 100644
--- a/datacenterlight/models.py
+++ b/datacenterlight/models.py
@@ -54,6 +54,7 @@ class VMPricing(models.Model):
discount_amount = models.DecimalField(
max_digits=6, decimal_places=2, default=0
)
+ stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
display_str = self.name + ' => ' + ' - '.join([
diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js
index 8fea438a..c6869cda 100644
--- a/datacenterlight/static/datacenterlight/js/main.js
+++ b/datacenterlight/static/datacenterlight/js/main.js
@@ -225,8 +225,8 @@
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
- (cardPricing['storage'].value * window.ssdUnitPrice) -
- window.discountAmount;
+ (cardPricing['storage'].value * window.ssdUnitPrice) +
+ window.vmBasePrice - window.discountAmount;
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}
diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py
index 55be8099..899b506f 100644
--- a/datacenterlight/tasks.py
+++ b/datacenterlight/tasks.py
@@ -56,13 +56,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
"Running create_vm_task on {}".format(current_task.request.hostname))
vm_id = None
try:
- final_price = (
- specs.get('total_price') if 'total_price' in specs
- else specs.get('price')
- )
-
if 'pass' in user:
- on_user = user.get('email')
+ on_user = user.get('username')
on_pass = user.get('pass')
logger.debug("Using user {user} to create VM".format(user=on_user))
vm_name = None
@@ -92,107 +87,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
if vm_id is None:
raise Exception("Could not create VM")
- # Update HostingOrder with the created vm_id
- hosting_order = HostingOrder.objects.filter(id=order_id).first()
- error_msg = None
-
- try:
- hosting_order.vm_id = vm_id
- hosting_order.save()
- logger.debug(
- "Updated hosting_order {} with vm_id={}".format(
- hosting_order.id, vm_id
- )
- )
- except Exception as ex:
- error_msg = (
- "HostingOrder with id {order_id} not found. This means that "
- "the hosting order was not created and/or it is/was not "
- "associated with VM with id {vm_id}. Details {details}".format(
- order_id=order_id, vm_id=vm_id, details=str(ex)
- )
- )
- logger.error(error_msg)
-
- stripe_utils = StripeUtils()
- result = stripe_utils.set_subscription_metadata(
- subscription_id=hosting_order.subscription_id,
- metadata={"VM_ID": str(vm_id)}
- )
-
- if result.get('error') is not None:
- emsg = "Could not update subscription metadata for {sub}".format(
- sub=hosting_order.subscription_id
- )
- logger.error(emsg)
- if error_msg:
- error_msg += ". " + emsg
- else:
- error_msg = emsg
-
- vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
-
- context = {
- 'name': user.get('name'),
- 'email': user.get('email'),
- 'cores': specs.get('cpu'),
- 'memory': specs.get('memory'),
- 'storage': specs.get('disk_size'),
- 'price': final_price,
- 'template': template.get('name'),
- 'vm_name': vm.get('name'),
- 'vm_id': vm['vm_id'],
- 'order_id': order_id
- }
-
- if error_msg:
- context['errors'] = error_msg
- if 'pricing_name' in specs:
- context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
- name=specs['pricing_name']
- ))
- email_data = {
- 'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
- 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
- 'to': ['info@ungleich.ch'],
- 'body': "\n".join(
- ["%s=%s" % (k, v) for (k, v) in context.items()]),
- 'reply_to': [context['email']],
- }
- email = EmailMessage(**email_data)
- email.send()
-
- if 'pass' in user:
- lang = 'en-us'
- if user.get('language') is not None:
- logger.debug(
- "Language is set to {}".format(user.get('language')))
- lang = user.get('language')
- translation.activate(lang)
- # Send notification to the user as soon as VM has been booked
- context = {
- 'base_url': "{0}://{1}".format(user.get('request_scheme'),
- user.get('request_host')),
- 'order_url': reverse('hosting:invoices'),
- 'page_header': _(
- 'Your New VM %(vm_name)s at Data Center Light') % {
- 'vm_name': vm.get('name')},
- 'vm_name': vm.get('name')
- }
- email_data = {
- 'subject': context.get('page_header'),
- 'to': user.get('email'),
- 'context': context,
- 'template_name': 'new_booked_vm',
- 'template_path': 'hosting/emails/',
- 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
- }
- email = BaseEmail(**email_data)
- email.send()
-
- logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
- if vm_id > 0:
- get_or_create_vm_detail(custom_user, manager, vm_id)
+ handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
+ template)
except Exception as e:
logger.error(str(e))
try:
@@ -214,3 +110,127 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
return
return vm_id
+
+
+def handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
+ template):
+ """
+ Handle's setting up of the metadata in Stripe and database and sending of
+ emails to the user after VM creation
+
+ :param order_id: the hosting order id
+ :param vm_id: the id of the vm created
+ :param manager: the OpenNebula Manager instance
+ :param user: the user's dict passed to the celery task
+ :param specs: the specification's dict passed to the celery task
+ :param template: the template dict passed to the celery task
+
+ :return:
+ """
+
+ custom_user = CustomUser.objects.get(email=user.get('email'))
+ final_price = (
+ specs.get('total_price') if 'total_price' in specs
+ else specs.get('price')
+ )
+ # Update HostingOrder with the created vm_id
+ hosting_order = HostingOrder.objects.filter(id=order_id).first()
+ error_msg = None
+
+ try:
+ hosting_order.vm_id = vm_id
+ hosting_order.save()
+ logger.debug(
+ "Updated hosting_order {} with vm_id={}".format(
+ hosting_order.id, vm_id
+ )
+ )
+ except Exception as ex:
+ error_msg = (
+ "HostingOrder with id {order_id} not found. This means that "
+ "the hosting order was not created and/or it is/was not "
+ "associated with VM with id {vm_id}. Details {details}".format(
+ order_id=order_id, vm_id=vm_id, details=str(ex)
+ )
+ )
+ logger.error(error_msg)
+
+ stripe_utils = StripeUtils()
+ result = stripe_utils.set_subscription_metadata(
+ subscription_id=hosting_order.subscription_id,
+ metadata={"VM_ID": str(vm_id)}
+ )
+
+ if result.get('error') is not None:
+ emsg = "Could not update subscription metadata for {sub}".format(
+ sub=hosting_order.subscription_id
+ )
+ logger.error(emsg)
+ if error_msg:
+ error_msg += ". " + emsg
+ else:
+ error_msg = emsg
+
+ vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
+
+ context = {
+ 'name': user.get('name'),
+ 'email': user.get('email'),
+ 'cores': specs.get('cpu'),
+ 'memory': specs.get('memory'),
+ 'storage': specs.get('disk_size'),
+ 'price': final_price,
+ 'template': template.get('name'),
+ 'vm_name': vm.get('name'),
+ 'vm_id': vm['vm_id'],
+ 'order_id': order_id
+ }
+
+ if error_msg:
+ context['errors'] = error_msg
+ if 'pricing_name' in specs:
+ context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
+ name=specs['pricing_name']
+ ))
+ email_data = {
+ 'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
+ 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
+ 'to': ['dcl-orders@ungleich.ch'],
+ 'body': "\n".join(
+ ["%s=%s" % (k, v) for (k, v) in context.items()]),
+ 'reply_to': [context['email']],
+ }
+ email = EmailMessage(**email_data)
+ email.send()
+
+ if 'pass' in user:
+ lang = 'en-us'
+ if user.get('language') is not None:
+ logger.debug(
+ "Language is set to {}".format(user.get('language')))
+ lang = user.get('language')
+ translation.activate(lang)
+ # Send notification to the user as soon as VM has been booked
+ context = {
+ 'base_url': "{0}://{1}".format(user.get('request_scheme'),
+ user.get('request_host')),
+ 'order_url': reverse('hosting:invoices'),
+ 'page_header': _(
+ 'Your New VM %(vm_name)s at Data Center Light') % {
+ 'vm_name': vm.get('name')},
+ 'vm_name': vm.get('name')
+ }
+ email_data = {
+ 'subject': context.get('page_header'),
+ 'to': user.get('email'),
+ 'context': context,
+ 'template_name': 'new_booked_vm',
+ 'template_path': 'hosting/emails/',
+ 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
+ }
+ email = BaseEmail(**email_data)
+ email.send()
+
+ logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
+ if vm_id > 0:
+ get_or_create_vm_detail(custom_user, manager, vm_id)
diff --git a/datacenterlight/templates/datacenterlight/cms/calculator.html b/datacenterlight/templates/datacenterlight/cms/calculator.html
index 7b123a72..20a6664a 100644
--- a/datacenterlight/templates/datacenterlight/cms/calculator.html
+++ b/datacenterlight/templates/datacenterlight/cms/calculator.html
@@ -1,5 +1,5 @@
- {% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
+ {% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing vm_base_price=vm_base_price %}
\ No newline at end of file
diff --git a/datacenterlight/templates/datacenterlight/emails/welcome_user.html b/datacenterlight/templates/datacenterlight/emails/welcome_user.html
index 25185618..2044b2ee 100644
--- a/datacenterlight/templates/datacenterlight/emails/welcome_user.html
+++ b/datacenterlight/templates/datacenterlight/emails/welcome_user.html
@@ -28,7 +28,7 @@
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
- {% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
+ {% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
diff --git a/datacenterlight/templates/datacenterlight/emails/welcome_user.txt b/datacenterlight/templates/datacenterlight/emails/welcome_user.txt
index 772e51a5..06e8aa33 100644
--- a/datacenterlight/templates/datacenterlight/emails/welcome_user.txt
+++ b/datacenterlight/templates/datacenterlight/emails/welcome_user.txt
@@ -3,7 +3,7 @@
{% trans "Welcome to Data Center Light!" %}
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
-{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
+{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %}
diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html
index f64a9500..2c2b51dd 100644
--- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html
+++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html
@@ -9,6 +9,7 @@
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
window.discountAmount = {{vm_pricing.discount_amount|default:0}};
+ window.vmBasePrice = {{vm_base_price|default:0}};
window.minRam = {{min_ram}};
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html
index 02bce0ed..1f7a3cda 100644
--- a/datacenterlight/templates/datacenterlight/order_detail.html
+++ b/datacenterlight/templates/datacenterlight/order_detail.html
@@ -2,6 +2,14 @@
{% load staticfiles bootstrap3 i18n custom_tags humanize %}
{% block content %}
+
{% if messages %}
@@ -103,58 +111,61 @@
{% endif %}
-
-
-
-
-
- {% trans "Price Before VAT" %}
- {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF
-
-
-
-
-
-
-
-
-
-
-
-
{% trans "Pre VAT" %}
-
-
-
{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)
-
+ {% if generic_payment_details.exclude_vat_calculations %}
+ {% else %}
+
@@ -267,15 +278,16 @@
{% if generic_payment_details %}
{% if generic_payment_details.recurring %}
{% if generic_payment_details.recurring_interval == 'year' %}
-
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/year{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF/year{% endblocktrans %}.
{% else %}
-
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}
+ By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF/month{% endblocktrans %}.
{% endif %}
{% else %}
-
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.
+
{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ total_price }} CHF{% endblocktrans %}.
{% endif %}
{% else %}
-
{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.
+
{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.
{% endif %}
@@ -318,5 +330,14 @@
{%endblock%}
diff --git a/datacenterlight/templatetags/custom_tags.py b/datacenterlight/templatetags/custom_tags.py
index 0cb18e5b..120cabbf 100644
--- a/datacenterlight/templatetags/custom_tags.py
+++ b/datacenterlight/templatetags/custom_tags.py
@@ -6,7 +6,7 @@ from django.core.urlresolvers import resolve, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import activate, get_language, ugettext_lazy as _
-from hosting.models import GenericProduct
+from hosting.models import GenericProduct, HostingOrder
from utils.hosting_utils import get_ip_addresses
logger = logging.getLogger(__name__)
@@ -63,6 +63,41 @@ def escaped_line_break(value):
return value.replace("\\n", "\n")
+@register.filter('get_line_item_from_hosting_order_charge')
+def get_line_item_from_hosting_order_charge(hosting_order_id):
+ """
+ Returns ready-to-use "html" line item to be shown for a charge in the
+ invoice list page
+
+ :param hosting_order_id: the HostingOrder id
+ :return:
+ """
+ try:
+ hosting_order = HostingOrder.objects.get(id = hosting_order_id)
+ if hosting_order.stripe_charge_id:
+ return mark_safe("""
+
{% 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 729d115b..e2f6e13b 100644
--- a/hosting/views.py
+++ b/hosting/views.py
@@ -1,6 +1,7 @@
import logging
import uuid
from datetime import datetime
+from urllib.parse import quote
from time import sleep
import stripe
@@ -13,6 +14,7 @@ from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse_lazy, reverse
+from django.db.models import Q
from django.http import (
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
)
@@ -386,7 +388,7 @@ class PasswordResetConfirmView(HostingContextMixin,
user = CustomUser.objects.get(pk=uid)
opennebula_client = OpenNebulaManager(
- email=user.email,
+ email=user.username,
password=user.password,
)
@@ -478,7 +480,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
def delete(self, request, *args, **kwargs):
owner = self.request.user
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
pk = self.kwargs.get('pk')
@@ -532,7 +534,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
ssh_key.private_key.save(filename, content)
owner = self.request.user
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
keys = get_all_public_keys(request.user)
@@ -563,9 +565,11 @@ class SettingsView(LoginRequiredMixin, FormView):
stripe_customer = None
if hasattr(user, 'stripecustomer'):
stripe_customer = user.stripecustomer
- cards_list = UserCardDetail.get_all_cards_list(
- stripe_customer=stripe_customer
+ stripe_utils = StripeUtils()
+ cards_list_request = stripe_utils.get_available_payment_methods(
+ stripe_customer
)
+ cards_list = cards_list_request.get('response_object')
context.update({
'cards_list': cards_list,
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
@@ -576,48 +580,38 @@ class SettingsView(LoginRequiredMixin, FormView):
def post(self, request, *args, **kwargs):
if 'card' in request.POST and request.POST['card'] is not '':
card_id = escape(request.POST['card'])
- user_card_detail = UserCardDetail.objects.get(id=card_id)
UserCardDetail.set_default_card(
stripe_api_cus_id=request.user.stripecustomer.stripe_id,
- stripe_source_id=user_card_detail.card_id
+ stripe_source_id=card_id
)
+ stripe_utils = StripeUtils()
+ card_details = stripe_utils.get_cards_details_from_payment_method(
+ card_id
+ )
+ if not card_details.get('response_object'):
+ logger.debug("Could not find card %s in stripe" % card_id)
+ messages.add_message(request, messages.ERROR,
+ _("Could not set a default card."))
+ return HttpResponseRedirect(reverse_lazy('hosting:settings'))
+ card_details_response = card_details['response_object']
msg = _(
("Your {brand} card ending in {last4} set as "
"default card").format(
- brand=user_card_detail.brand,
- last4=user_card_detail.last4
+ brand=card_details_response['brand'],
+ last4=card_details_response['last4']
)
)
messages.add_message(request, messages.SUCCESS, msg)
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
if 'delete_card' in request.POST:
- try:
- card = UserCardDetail.objects.get(pk=self.kwargs.get('pk'))
- if (request.user.has_perm(self.permission_required[0], card)
- and
- request.user
- .stripecustomer
- .usercarddetail_set
- .count() > 1):
- if card.card_id is not None:
- stripe_utils = StripeUtils()
- stripe_utils.dissociate_customer_card(
- request.user.stripecustomer.stripe_id,
- card.card_id
- )
- if card.preferred:
- UserCardDetail.set_default_card_from_stripe(
- request.user.stripecustomer.stripe_id
- )
- card.delete()
- msg = _("Card deassociation successful")
- messages.add_message(request, messages.SUCCESS, msg)
- else:
- msg = _("You are not permitted to do this operation")
- messages.add_message(request, messages.ERROR, msg)
- except UserCardDetail.DoesNotExist:
- msg = _("The selected card does not exist")
- messages.add_message(request, messages.ERROR, msg)
+ card = self.kwargs.get('pk')
+ stripe_utils = StripeUtils()
+ stripe_utils.dissociate_customer_card(
+ request.user.stripecustomer.stripe_id,
+ card
+ )
+ msg = _("Card deassociation successful")
+ messages.add_message(request, messages.SUCCESS, msg)
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
form = self.get_form()
if form.is_valid():
@@ -692,51 +686,49 @@ class SettingsView(LoginRequiredMixin, FormView):
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
else:
- token = form.cleaned_data.get('token')
+ id_payment_method = request.POST.get('id_payment_method', None)
stripe_utils = StripeUtils()
- card_details = stripe_utils.get_cards_details_from_token(
- token
+ card_details = stripe_utils.get_cards_details_from_payment_method(
+ id_payment_method
)
if not card_details.get('response_object'):
form.add_error("__all__", card_details.get('error'))
return self.render_to_response(self.get_context_data())
stripe_customer = StripeCustomer.get_or_create(
- email=request.user.email, token=token
+ email=request.user.email, id_payment_method=id_payment_method
)
card = card_details['response_object']
- if UserCardDetail.get_user_card_details(stripe_customer, card):
- msg = _('You seem to have already added this card')
- messages.add_message(request, messages.ERROR, msg)
- else:
- acc_result = stripe_utils.associate_customer_card(
- request.user.stripecustomer.stripe_id, token
- )
- if acc_result['response_object'] is None:
- msg = _(
- 'An error occurred while associating the card.'
- ' Details: {details}'.format(
- details=acc_result['error']
- )
- )
- messages.add_message(request, messages.ERROR, msg)
- return self.render_to_response(self.get_context_data())
- preferred = False
- if stripe_customer.usercarddetail_set.count() == 0:
- preferred = True
- UserCardDetail.create(
- stripe_customer=stripe_customer,
- last4=card['last4'],
- brand=card['brand'],
- fingerprint=card['fingerprint'],
- exp_month=card['exp_month'],
- exp_year=card['exp_year'],
- card_id=card['card_id'],
- preferred=preferred
- )
+ acc_result = stripe_utils.associate_customer_card(
+ request.user.stripecustomer.stripe_id,
+ id_payment_method,
+ set_as_default=True
+ )
+ if acc_result['response_object'] is None:
msg = _(
- "Successfully associated the card with your account"
+ 'An error occurred while associating the card.'
+ ' Details: {details}'.format(
+ details=acc_result['error']
+ )
)
- messages.add_message(request, messages.SUCCESS, msg)
+ messages.add_message(request, messages.ERROR, msg)
+ return self.render_to_response(self.get_context_data())
+ preferred = False
+ if stripe_customer.usercarddetail_set.count() == 0:
+ preferred = True
+ UserCardDetail.create(
+ stripe_customer=stripe_customer,
+ last4=card['last4'],
+ brand=card['brand'],
+ fingerprint=card['fingerprint'],
+ exp_month=card['exp_month'],
+ exp_year=card['exp_year'],
+ card_id=card['card_id'],
+ preferred=preferred
+ )
+ msg = _(
+ "Successfully associated the card with your account"
+ )
+ messages.add_message(request, messages.SUCCESS, msg)
return self.render_to_response(self.get_context_data())
else:
billing_address_data = form.cleaned_data
@@ -952,7 +944,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
except VMDetail.DoesNotExist:
try:
manager = OpenNebulaManager(
- email=owner.email, password=owner.password
+ email=owner.username, password=owner.password
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
@@ -1077,7 +1069,13 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
billing_address_data = request.session.get('billing_address_data')
vm_template_id = template.get('id', 1)
stripe_api_cus_id = request.user.stripecustomer.stripe_id
- if 'token' in self.request.session:
+ logger.debug("template=%s specs=%s stripe_customer_id=%s "
+ "billing_address_data=%s vm_template_id=%s "
+ "stripe_api_cus_id=%s" % (
+ template, specs, stripe_customer_id, billing_address_data,
+ vm_template_id, stripe_api_cus_id)
+ )
+ if 'id_payment_method' in self.request.session:
card_details = stripe_utils.get_cards_details_from_token(
request.session['token']
)
@@ -1094,7 +1092,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
)
if not ucd:
acc_result = stripe_utils.associate_customer_card(
- stripe_api_cus_id, request.session['token'],
+ stripe_api_cus_id, request.session['id_payment_method'],
set_as_default=True
)
if acc_result['response_object'] is None:
@@ -1185,10 +1183,31 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
- coupon='ipv6-discount-8chf' if 'name' in discount and 'ipv6' in discount['name'].lower() else "",
+ coupon=(discount['stripe_coupon_id']
+ if 'name' in discount and
+ discount['name'] is not None and
+ 'ipv6' in discount['name'].lower() and
+ discount['stripe_coupon_id']
+ else ""),
tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [],
+ default_payment_method=request.session['id_payment_method']
)
stripe_subscription_obj = subscription_result.get('response_object')
+ latest_invoice = stripe.Invoice.retrieve(stripe_subscription_obj.latest_invoice)
+ ret = stripe.PaymentIntent.confirm(
+ latest_invoice.payment_intent
+ )
+ if ret.status == 'requires_source_action' or ret.status == 'requires_action':
+ pi = stripe.PaymentIntent.retrieve(
+ latest_invoice.payment_intent
+ )
+ context = {
+ 'sid': stripe_subscription_obj.id,
+ 'payment_intent_secret': pi.client_secret,
+ 'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY,
+ 'showSCA': True
+ }
+ return JsonResponse(context)
# Check if the subscription was approved and is active
if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'active'):
@@ -1227,6 +1246,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
user = {
'name': self.request.user.name,
'email': self.request.user.email,
+ 'username': self.request.user.username,
'pass': self.request.user.password,
'request_scheme': request.scheme,
'request_host': request.get_host(),
@@ -1281,10 +1301,11 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
page = self.request.GET.get('page', 1)
context = super(InvoiceListView, self).get_context_data(**kwargs)
invs_page = None
+ invs_page_charges = None
if ('user_email' in self.request.GET
and self.request.user.email == settings.ADMIN_EMAIL):
user_email = self.request.GET['user_email']
- context['user_email'] = user_email
+ context['user_email'] = '%s' % quote(user_email)
logger.debug(
"user_email = {}".format(user_email)
)
@@ -1294,7 +1315,8 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
logger.debug("User does not exist")
cu = self.request.user
invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id,
- count=100)
+ count=100,
+ status='paid')
paginator = Paginator(invs.data, 10)
try:
invs_page = paginator.page(page)
@@ -1302,6 +1324,21 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
+ hosting_orders = HostingOrder.objects.filter(
+ customer=cu.stripecustomer).filter(
+ Q(subscription_id=None) | Q(subscription_id='')
+ ).order_by('-created_at')
+ stripe_chgs = []
+ for ho in hosting_orders:
+ stripe_chgs.append({ho.id: stripe.Charge.retrieve(ho.stripe_charge_id)})
+
+ paginator_charges = Paginator(stripe_chgs, 10)
+ try:
+ invs_page_charges = paginator_charges.page(page)
+ except PageNotAnInteger:
+ invs_page_charges = paginator_charges.page(1)
+ except EmptyPage:
+ invs_page_charges = paginator_charges.page(paginator_charges.num_pages)
else:
try:
invs = stripe.Invoice.list(
@@ -1315,10 +1352,27 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
+ hosting_orders = HostingOrder.objects.filter(
+ customer=self.request.user.stripecustomer).filter(
+ Q(subscription_id=None) | Q(subscription_id='')
+ ).order_by('-created_at')
+ stripe_chgs = []
+ for ho in hosting_orders:
+ stripe_chgs.append(
+ {ho: stripe.Charge.retrieve(ho.stripe_charge_id)})
+ paginator_charges = Paginator(stripe_chgs, 10)
+ try:
+ invs_page_charges = paginator_charges.page(page)
+ except PageNotAnInteger:
+ invs_page_charges = paginator_charges.page(1)
+ except EmptyPage:
+ invs_page_charges = paginator_charges.page(
+ paginator_charges.num_pages)
except Exception as ex:
logger.error(str(ex))
invs_page = None
context["invs"] = invs_page
+ context["invs_charge"] = invs_page_charges
return context
@method_decorator(decorators)
@@ -1399,7 +1453,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
# fallback to get it from the infrastructure
try:
manager = OpenNebulaManager(
- email=self.request.user.email,
+ email=self.request.user.username,
password=self.request.user.password
)
vm = manager.get_vm(vm_id)
@@ -1482,7 +1536,7 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
def get_queryset(self):
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
try:
queryset = manager.get_vms()
@@ -1643,7 +1697,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
owner = self.request.user
vm = None
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
vm_id = self.kwargs.get('pk')
@@ -1727,7 +1781,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
vm = self.get_object()
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
try:
@@ -1755,7 +1809,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
error_msg = result.get('error')
logger.error(
'Error canceling subscription for {user} and vm id '
- '{vm_id}'.format(user=owner.email, vm_id=vm.id)
+ '{vm_id}'.format(user=owner.username, vm_id=vm.id)
)
logger.error(error_msg)
admin_email_body['stripe_error_msg'] = error_msg
@@ -1777,7 +1831,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
response['text'] = str(_('Error terminating VM')) + str(vm.id)
else:
- for t in range(15):
+ for t in range(settings.MAX_TIME_TO_WAIT_FOR_VM_TERMINATE):
try:
manager.get_vm(vm.id)
except WrongIdError:
@@ -1800,6 +1854,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
break
else:
+ logger.debug(
+ 'Sleeping 2 seconds for terminate action on VM %s' %
+ vm.id
+ )
sleep(2)
if not response['status']:
response['text'] = str(_("VM terminate action timed out. "
@@ -1827,6 +1885,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
email.send()
admin_email_body.update(response)
admin_email_body["customer_email"] = owner.email
+ admin_email_body["customer_username"] = owner.username
admin_email_body["VM_ID"] = vm.id
admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if
hosting_order is not None
@@ -1844,15 +1903,15 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
admin_email_body["subscription_amount"] = total_amount/100
admin_email_body["subscription_detail"] = content
- admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
+ admin_msg_sub = "VM and Subscription for VM {} and user: {}, {}".format(
vm.id,
- owner.email
+ owner.email, owner.username
)
email_to_admin_data = {
'subject': ("Deleted " if response['status']
else "ERROR deleting ") + admin_msg_sub,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
- 'to': ['info@ungleich.ch'],
+ 'to': ['dcl-orders@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
}
@@ -1892,7 +1951,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin,
context = super(DetailView, self).get_context_data(**kwargs)
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# Get vms
queryset = manager.get_vms()
diff --git a/membership/models.py b/membership/models.py
index 80aaf408..81054fb9 100644
--- a/membership/models.py
+++ b/membership/models.py
@@ -233,8 +233,17 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
ldap_manager.create_user(self.username, password=password,
firstname=first_name, lastname=last_name,
email=self.email)
- self.in_ldap = True
- self.save()
+ else:
+ # User exists already in LDAP, but with a dummy credential
+ # We are here implies that the user has successfully
+ # authenticated against Django db, and a corresponding user
+ # exists in LDAP.
+ # We just update the LDAP credentials once again, assuming it
+ # was set to a dummy value while migrating users from Django to
+ # LDAP
+ ldap_manager.change_password(self.username, password)
+ self.in_ldap = True
+ self.save()
def __str__(self): # __unicode__ on Python 2
return self.email
@@ -268,7 +277,7 @@ class StripeCustomer(models.Model):
return "%s - %s" % (self.stripe_id, self.user.email)
@classmethod
- def create_stripe_api_customer(cls, email=None, token=None,
+ def create_stripe_api_customer(cls, email=None, id_payment_method=None,
customer_name=None):
"""
This method creates a Stripe API customer with the given
@@ -279,7 +288,8 @@ class StripeCustomer(models.Model):
stripe user.
"""
stripe_utils = StripeUtils()
- stripe_data = stripe_utils.create_customer(token, email, customer_name)
+ stripe_data = stripe_utils.create_customer(
+ id_payment_method, email, customer_name)
if stripe_data.get('response_object'):
stripe_cus_id = stripe_data.get('response_object').get('id')
return stripe_cus_id
@@ -287,7 +297,7 @@ class StripeCustomer(models.Model):
return None
@classmethod
- def get_or_create(cls, email=None, token=None):
+ def get_or_create(cls, email=None, token=None, id_payment_method=None):
"""
Check if there is a registered stripe customer with that email
or create a new one
diff --git a/opennebula_api/models.py b/opennebula_api/models.py
index 19e3e4f7..2f76f423 100644
--- a/opennebula_api/models.py
+++ b/opennebula_api/models.py
@@ -154,6 +154,8 @@ class OpenNebulaManager():
protocol=settings.OPENNEBULA_PROTOCOL)
)
raise ConnectionRefusedError
+ except Exception as ex:
+ logger.error(str(ex))
def _get_user_pool(self):
try:
@@ -427,8 +429,12 @@ class OpenNebulaManager():
template_id = int(template_id)
try:
template_pool = self._get_template_pool()
+ if template_id in settings.UPDATED_TEMPLATES_DICT.keys():
+ template_id = settings.UPDATED_TEMPLATES_DICT[template_id]
return template_pool.get_by_id(template_id)
- except:
+ except Exception as ex:
+ logger.debug("Template Id we are looking for : %s" % template_id)
+ logger.error(str(ex))
raise ConnectionRefusedError
def create_template(self, name, cores, memory, disk_size, core_price,
diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py
index c7418aa5..34cdde7c 100644
--- a/opennebula_api/serializers.py
+++ b/opennebula_api/serializers.py
@@ -86,7 +86,7 @@ class VirtualMachineSerializer(serializers.Serializer):
}
try:
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password,
)
opennebula_id = manager.create_vm(template_id=template_id,
diff --git a/opennebula_api/views.py b/opennebula_api/views.py
index 9bf03a74..318fa32e 100644
--- a/opennebula_api/views.py
+++ b/opennebula_api/views.py
@@ -19,7 +19,7 @@ class VmCreateView(generics.ListCreateAPIView):
def get_queryset(self):
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@@ -42,7 +42,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self):
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@@ -54,7 +54,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_object(self):
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@@ -66,7 +66,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def perform_destroy(self, instance):
owner = self.request.user
- manager = OpenNebulaManager(email=owner.email,
+ manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
diff --git a/release.sh b/release.sh
new file mode 100755
index 00000000..535cc7d4
--- /dev/null
+++ b/release.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+# Nico Schottelius, 2021-12-17
+
+current=$(git describe --dirty)
+last_tag=$(git describe --tags --abbrev=0)
+registry=harbor.ungleich.svc.p10.k8s.ooo/ungleich-public
+image_url=$registry/dynamicweb:${current}
+
+if echo $current | grep -q -e 'dirty$'; then
+ echo Refusing to release a dirty tree build
+ exit 1
+fi
+
+if [ "$current" != "$last_tag" ]; then
+ echo "Last tag ($last_tag) is not current version ($current)"
+ echo "Only release proper versions"
+ exit 1
+fi
+
+docker tag dynamicweb:${current} ${image_url}
+docker push ${image_url}
diff --git a/requirements.txt b/requirements.txt
index e7769a7e..8d04a189 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,7 +25,7 @@ django-compressor==2.0
django-debug-toolbar==1.4
python-dotenv==0.10.3
django-extensions==1.6.7
-django-filer==1.2.0
+django-filer==2.1.2
django-filter==0.13.0
django-formtools==1.0
django-guardian==1.4.4
@@ -83,7 +83,7 @@ stripe==2.41.0
wheel==0.29.0
django-admin-honeypot==1.0.0
coverage==4.3.4
-git+https://github.com/ungleich/python-oca.git#egg=python-oca
+git+https://github.com/ungleich/python-oca.git#egg=oca
djangorestframework==3.6.3
flake8==3.3.0
python-memcached==1.58
diff --git a/templates/gdpr/gdpr_banner.html b/templates/gdpr/gdpr_banner.html
index 7e9f5c7f..f927f8ee 100644
--- a/templates/gdpr/gdpr_banner.html
+++ b/templates/gdpr/gdpr_banner.html
@@ -134,8 +134,6 @@
digitalglarus.ch
hack4lgarus.ch
ipv6onlyhosting.com
- ipv6onlyhosting.ch
- ipv6onlyhosting.net
django-hosting.ch
rails-hosting.ch
node-hosting.ch
diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py
index 7bff9a89..b9e2eb8a 100644
--- a/utils/hosting_utils.py
+++ b/utils/hosting_utils.py
@@ -3,6 +3,8 @@ import logging
import math
import subprocess
+from django.conf import settings
+
from oca.pool import WrongIdError
from datacenterlight.models import VMPricing
@@ -79,7 +81,8 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(disk_size) * pricing.ssd_unit_price) +
- (decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
+ (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
+ decimal.Decimal(settings.VM_BASE_PRICE))
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
return round(float(price), 2)
@@ -102,7 +105,8 @@ def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
- (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
+ (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
+ decimal.Decimal(settings.VM_BASE_PRICE)
)
discount_name = pricing.discount_name
@@ -118,7 +122,8 @@ def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
discount = {
'name': discount_name,
'amount': discount_amount,
- 'amount_with_vat': round(float(discount_amount_with_vat), 2)
+ 'amount_with_vat': round(float(discount_amount_with_vat), 2),
+ 'stripe_coupon_id': pricing.stripe_coupon_id
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
@@ -154,7 +159,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
- (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
+ (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
+ decimal.Decimal(settings.VM_BASE_PRICE)
)
if pricing.vat_inclusive:
vat = decimal.Decimal(0)
@@ -168,7 +174,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': pricing.discount_name,
- 'amount': round(float(pricing.discount_amount), 2)
+ 'amount': round(float(pricing.discount_amount), 2),
+ 'stripe_coupon_id': pricing.stripe_coupon_id
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
@@ -215,11 +222,6 @@ def get_ip_addresses(vm_id):
return "--"
-def round_up(n, decimals=0):
- multiplier = 10 ** decimals
- return math.ceil(n * multiplier) / multiplier
-
-
class HostingUtils:
@staticmethod
def clear_items_from_list(from_list, items_list):
diff --git a/utils/ldap_manager.py b/utils/ldap_manager.py
index fd039ad5..d40e931f 100644
--- a/utils/ldap_manager.py
+++ b/utils/ldap_manager.py
@@ -3,6 +3,7 @@ import hashlib
import random
import ldap3
import logging
+import unicodedata
from django.conf import settings
@@ -87,7 +88,7 @@ class LdapManager:
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
- uid = user.encode("utf-8")
+ uid = user
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),
@@ -101,7 +102,7 @@ class LdapManager:
"uidNumber": [str(uidNumber)],
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
"loginShell": ["/bin/bash"],
- "homeDirectory": ["/home/{}".format(user).encode("utf-8")],
+ "homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))],
"mail": email.encode("utf-8"),
"userPassword": [self._ssha_password(
password.encode("utf-8")
@@ -266,7 +267,7 @@ class LdapManager:
logger.error(
"Error reading int value from {}. {}"
"Returning default value {} instead".format(
- settings.LDAP_MAX_UID_PATH,
+ settings.LDAP_MAX_UID_FILE_PATH,
str(ve),
settings.LDAP_DEFAULT_START_UID
)
diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py
index ade06dd3..875a174e 100644
--- a/utils/stripe_utils.py
+++ b/utils/stripe_utils.py
@@ -34,6 +34,7 @@ def handleStripeError(f):
logger.error(str(e))
return response
except stripe.error.RateLimitError as e:
+ logger.error(str(e))
response.update(
{'error': "Too many requests made to the API too quickly"})
return response
@@ -69,7 +70,7 @@ class StripeUtils(object):
CURRENCY = 'chf'
INTERVAL = 'month'
SUCCEEDED_STATUS = 'succeeded'
- STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
+ RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists'
STRIPE_NO_SUCH_PLAN = 'No such plan'
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
@@ -82,20 +83,31 @@ class StripeUtils(object):
customer.save()
@handleStripeError
- def associate_customer_card(self, stripe_customer_id, token,
+ def associate_customer_card(self, stripe_customer_id, id_payment_method,
set_as_default=False):
customer = stripe.Customer.retrieve(stripe_customer_id)
- card = customer.sources.create(source=token)
+ stripe.PaymentMethod.attach(
+ id_payment_method,
+ customer=stripe_customer_id,
+ )
if set_as_default:
- customer.default_source = card.id
+ customer.invoice_settings.default_payment_method = id_payment_method
customer.save()
return True
@handleStripeError
def dissociate_customer_card(self, stripe_customer_id, card_id):
customer = stripe.Customer.retrieve(stripe_customer_id)
- card = customer.sources.retrieve(card_id)
- card.delete()
+ if card_id.startswith("pm"):
+ logger.debug("PaymentMethod %s detached %s" % (card_id,
+ stripe_customer_id))
+ pm = stripe.PaymentMethod.retrieve(card_id)
+ stripe.PaymentMethod.detach(card_id)
+ pm.delete()
+ else:
+ logger.debug("card %s detached %s" % (card_id, stripe_customer_id))
+ card = customer.sources.retrieve(card_id)
+ card.delete()
@handleStripeError
def update_customer_card(self, customer_id, token):
@@ -187,6 +199,24 @@ class StripeUtils(object):
}
return card_details
+ @handleStripeError
+ def get_cards_details_from_payment_method(self, payment_method_id):
+ payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
+ # payment_method does not always seem to have a card with id
+ # if that is the case, fallback to payment_method_id for card_id
+ card_id = payment_method_id
+ if hasattr(payment_method.card, 'id'):
+ card_id = payment_method.card.id
+ card_details = {
+ 'last4': payment_method.card.last4,
+ 'brand': payment_method.card.brand,
+ 'exp_month': payment_method.card.exp_month,
+ 'exp_year': payment_method.card.exp_year,
+ 'fingerprint': payment_method.card.fingerprint,
+ 'card_id': card_id
+ }
+ return card_details
+
def check_customer(self, stripe_cus_api_id, user, token):
try:
customer = stripe.Customer.retrieve(stripe_cus_api_id)
@@ -206,11 +236,11 @@ class StripeUtils(object):
return customer
@handleStripeError
- def create_customer(self, token, email, name=None):
+ def create_customer(self, id_payment_method, email, name=None):
if name is None or name.strip() == "":
name = email
customer = self.stripe.Customer.create(
- source=token,
+ payment_method=id_payment_method,
description=name,
email=email
)
@@ -267,11 +297,17 @@ class StripeUtils(object):
stripe_plan_db_obj = StripePlan.objects.create(
stripe_plan_id=stripe_plan_id)
except stripe.error.InvalidRequestError as e:
- if self.STRIPE_PLAN_ALREADY_EXISTS in str(e):
+ logger.error(str(e))
+ logger.error("error_code = %s" % str(e.__dict__))
+ if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code:
logger.debug(
self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
- stripe_plan_db_obj = StripePlan.objects.create(
+ stripe_plan_db_obj, c = StripePlan.objects.get_or_create(
stripe_plan_id=stripe_plan_id)
+ if c:
+ logger.debug("Created stripe plan %s" % stripe_plan_id)
+ else:
+ logger.debug("Plan %s exists already" % stripe_plan_id)
return stripe_plan_db_obj
@handleStripeError
@@ -300,10 +336,14 @@ class StripeUtils(object):
@handleStripeError
def subscribe_customer_to_plan(self, customer, plans, trial_end=None,
- coupon="", tax_rates=list()):
+ coupon="", tax_rates=list(),
+ default_payment_method=""):
"""
Subscribes the given customer to the list of given plans
+ :param default_payment_method:
+ :param tax_rates:
+ :param coupon:
:param customer: The stripe customer identifier
:param plans: A list of stripe plans.
:param trial_end: An integer representing when the Stripe subscription
@@ -317,12 +357,17 @@ class StripeUtils(object):
]
:return: The subscription StripeObject
"""
-
+ logger.debug("Subscribing %s to plan %s : coupon = %s" % (
+ customer, str(plans), str(coupon)
+ ))
subscription_result = self.stripe.Subscription.create(
customer=customer, items=plans, trial_end=trial_end,
coupon=coupon,
default_tax_rates=tax_rates,
+ payment_behavior='allow_incomplete',
+ default_payment_method=default_payment_method
)
+ logger.debug("Done subscribing")
return subscription_result
@handleStripeError
@@ -480,7 +525,49 @@ class StripeUtils(object):
)
return tax_id_obj
+ @handleStripeError
+ def get_payment_intent(self, amount, customer):
+ """ Create a stripe PaymentIntent of the given amount and return it
+ :param amount: the amount of payment_intent
+ :return:
+ """
+ payment_intent_obj = stripe.PaymentIntent.create(
+ amount=amount,
+ currency='chf',
+ customer=customer,
+ setup_future_usage='off_session'
+ )
+ return payment_intent_obj
+
+ @handleStripeError
+ def get_available_payment_methods(self, customer):
+ """ Retrieves all payment methods of the given customer
+ :param customer: StripeCustomer object
+ :return: a list of available payment methods
+ """
+ return_list = []
+ if customer is None:
+ return return_list
+ cu = stripe.Customer.retrieve(customer.stripe_id)
+ pms = stripe.PaymentMethod.list(
+ customer=customer.stripe_id,
+ type="card",
+ )
+ default_source = None
+ if cu.default_source:
+ default_source = cu.default_source
+ else:
+ default_source = cu.invoice_settings.default_payment_method
+ for pm in pms.data:
+ return_list.append({
+ 'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id,
+ 'exp_year': pm.card.exp_year,
+ 'exp_month': '{:02d}'.format(pm.card.exp_month),
+ 'preferred': pm.id == default_source
+ })
+ return return_list
+
def compare_vat_numbers(self, vat1, vat2):
_vat1 = vat1.replace(" ", "").replace(".", "").replace("-","")
_vat2 = vat2.replace(" ", "").replace(".", "").replace("-","")
- return True if _vat1 == _vat2 else False
\ No newline at end of file
+ return True if _vat1 == _vat2 else False
diff --git a/utils/views.py b/utils/views.py
index 05d0fdc2..f30a349a 100644
--- a/utils/views.py
+++ b/utils/views.py
@@ -228,7 +228,7 @@ class SSHKeyCreateView(FormView):
if self.request.user.is_authenticated():
owner = self.request.user
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
keys_to_save = get_all_public_keys(self.request.user)
diff --git a/vat_rates.csv b/vat_rates.csv
index 17bdb997..72870530 100644
--- a/vat_rates.csv
+++ b/vat_rates.csv
@@ -321,5 +321,4 @@ IM",GBP,0.1,standard,
2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually)
2019-12-17,,FX,EUR,0.20,standard,France metropolitan standard VAT (added manually)
2020-01-04,,CY,EUR,0.19,standard,Cyprus standard VAT (added manually)
-2019-01-04,,IL,EUR,0.23,standard,Ireland standard VAT (added manually)
2019-01-04,,LI,EUR,0.077,standard,Liechtenstein standard VAT (added manually)
diff --git a/webhook/views.py b/webhook/views.py
index 516d1afc..0a96d0b6 100644
--- a/webhook/views.py
+++ b/webhook/views.py
@@ -1,14 +1,17 @@
import datetime
import logging
-
+import json
import stripe
+
# Create your views here.
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
+from datacenterlight.views import do_provisioning, do_provisioning_generic
from membership.models import StripeCustomer
+from hosting.models import IncompleteSubscriptions, IncompletePaymentIntents
from utils.models import BillingAddress, UserBillingAddress
from utils.tasks import send_plain_email_task
@@ -111,8 +114,193 @@ def handle_webhook(request):
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(tax_id_obj),
}
-
send_plain_email_task.delay(email_data)
+ elif event.type == 'invoice.paid':
+ #More info: https://stripe.com/docs/billing/migration/strong-customer-authentication#scenario-1-handling-fulfillment
+ invoice_obj = event.data.object
+ logger.debug("Webhook Event: invoice.paid")
+ logger.debug("invoice_obj %s " % str(invoice_obj))
+ logger.debug("invoice_obj.paid = %s %s" % (invoice_obj.paid, type(invoice_obj.paid)))
+ logger.debug("invoice_obj.billing_reason = %s %s" % (invoice_obj.billing_reason, type(invoice_obj.billing_reason)))
+ # We should check for billing_reason == "subscription_create" but we
+ # check for "subscription_update"
+ # because we are using older api.
+ # See https://stripe.com/docs/upgrades?since=2015-07-13
+
+ # The billing_reason attribute of the invoice object now can take the
+ # value of subscription_create, indicating that it is the first
+ # invoice of a subscription. For older API versions,
+ # billing_reason=subscription_create is represented as
+ # subscription_update.
+
+ if (invoice_obj.paid and
+ invoice_obj.billing_reason == "subscription_update"):
+ logger.debug("""invoice_obj.paid and
+ invoice_obj.billing_reason == subscription_update""")
+ logger.debug("Start provisioning")
+ try:
+ logger.debug("Looking for subscription %s" %
+ invoice_obj.subscription)
+ stripe_subscription_obj = stripe.Subscription.retrieve(
+ invoice_obj.subscription)
+ try:
+ incomplete_sub = IncompleteSubscriptions.objects.get(
+ subscription_id=invoice_obj.subscription)
+ request = ""
+ soc = ""
+ card_details_response = ""
+ gp_details = ""
+ template = ""
+ specs = ""
+ billing_address_data = ""
+ if incomplete_sub.request:
+ request = json.loads(incomplete_sub.request)
+ if incomplete_sub.specs:
+ specs = json.loads(incomplete_sub.specs)
+ if incomplete_sub.stripe_onetime_charge:
+ soc = json.loads(incomplete_sub.stripe_onetime_charge)
+ if incomplete_sub.gp_details:
+ gp_details = json.loads(incomplete_sub.gp_details)
+ if incomplete_sub.card_details_response:
+ card_details_response = json.loads(
+ incomplete_sub.card_details_response)
+ if incomplete_sub.template:
+ template = json.loads(
+ incomplete_sub.template)
+ if incomplete_sub.billing_address_data:
+ billing_address_data = json.loads(
+ incomplete_sub.billing_address_data)
+ logger.debug("*******")
+ logger.debug(str(incomplete_sub))
+ logger.debug("*******")
+ logger.debug("1*******")
+ logger.debug(request)
+ logger.debug("2*******")
+ logger.debug(card_details_response)
+ logger.debug("3*******")
+ logger.debug(soc)
+ logger.debug("4*******")
+ logger.debug(gp_details)
+ logger.debug("5*******")
+ logger.debug(template)
+ logger.debug("6*******")
+ do_provisioning(
+ request=request,
+ stripe_api_cus_id=incomplete_sub.stripe_api_cus_id,
+ card_details_response=card_details_response,
+ stripe_subscription_obj=stripe_subscription_obj,
+ stripe_onetime_charge=soc,
+ gp_details=gp_details,
+ specs=specs,
+ vm_template_id=incomplete_sub.vm_template_id,
+ template=template,
+ billing_address_data=billing_address_data,
+ real_request=None
+ )
+ except IncompleteSubscriptions.DoesNotExist as ex:
+ logger.error(str(ex))
+ except IncompleteSubscriptions.MultipleObjectsReturned as ex:
+ logger.error(str(ex))
+ email_data = {
+ 'subject': "IncompleteSubscriptions error",
+ 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
+ 'to': settings.DCL_ERROR_EMAILS_TO_LIST,
+ 'body': "Response = %s" % str(ex),
+ }
+ send_plain_email_task.delay(email_data)
+ except Exception as ex:
+ logger.error(str(ex))
+ email_data = {
+ 'subject': "invoice.paid Webhook error",
+ 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
+ 'to': settings.DCL_ERROR_EMAILS_TO_LIST,
+ 'body': "Response = %s" % str(ex),
+ }
+ send_plain_email_task.delay(email_data)
+ elif event.type == 'invoice.payment_failed':
+ invoice_obj = event.data.object
+ logger.debug("Webhook Event: invoice.payment_failed")
+ logger.debug("invoice_obj %s " % str(invoice_obj))
+ if (invoice_obj.payment_failed and
+ invoice_obj.billing_reason == "subscription_update"):
+ logger.debug("Payment failed, inform the users")
+ elif event.type == 'payment_intent.succeeded':
+ payment_intent_obj = event.data.object
+ logger.debug("Webhook Event: payment_intent.succeeded")
+ logger.debug("payment_intent_obj %s " % str(payment_intent_obj))
+ try:
+ logger.debug("Looking for IncompletePaymentIntents %s " %
+ payment_intent_obj.id)
+ incomplete_pm = IncompletePaymentIntents.objects.get(
+ payment_intent_id=payment_intent_obj.id)
+ logger.debug("incomplete_pm = %s" % str(incomplete_pm.__dict__))
+ request = ""
+ soc = ""
+ card_details_response = ""
+ gp_details = ""
+ template = ""
+ billing_address_data = ""
+ if incomplete_pm.request:
+ request = json.loads(incomplete_pm.request)
+ logger.debug("request = %s" % str(request))
+ if incomplete_pm.stripe_charge_id:
+ soc = incomplete_pm.stripe_charge_id
+ logger.debug("stripe_onetime_charge = %s" % str(soc))
+ if incomplete_pm.gp_details:
+ gp_details = json.loads(incomplete_pm.gp_details)
+ logger.debug("gp_details = %s" % str(gp_details))
+ if incomplete_pm.card_details_response:
+ card_details_response = json.loads(
+ incomplete_pm.card_details_response)
+ logger.debug("card_details_response = %s" % str(card_details_response))
+ if incomplete_pm.billing_address_data:
+ billing_address_data = json.loads(
+ incomplete_pm.billing_address_data)
+ logger.debug("billing_address_data = %s" % str(billing_address_data))
+ logger.debug("1*******")
+ logger.debug(request)
+ logger.debug("2*******")
+ logger.debug(card_details_response)
+ logger.debug("3*******")
+ logger.debug(soc)
+ logger.debug("4*******")
+ logger.debug(gp_details)
+ logger.debug("5*******")
+ logger.debug(template)
+ logger.debug("6*******")
+ logger.debug(billing_address_data)
+ incomplete_pm.completed_at = datetime.datetime.now()
+ charges = ""
+ if len(payment_intent_obj.charges.data) > 0:
+ for d in payment_intent_obj.charges.data:
+ if charges == "":
+ charges = "%s" % d.id
+ else:
+ charges = "%s,%s" % (charges, d.id)
+ logger.debug("Charge ids = %s" % charges)
+ incomplete_pm.stripe_charge_id=charges
+ do_provisioning_generic(
+ request=request,
+ stripe_api_cus_id=incomplete_pm.stripe_api_cus_id,
+ card_details_response=card_details_response,
+ stripe_subscription_id=None,
+ stripe_charge_id=charges,
+ gp_details=gp_details,
+ billing_address_data=billing_address_data
+ )
+ incomplete_pm.save()
+ except IncompletePaymentIntents.DoesNotExist as ex:
+ logger.error(str(ex))
+ except (IncompletePaymentIntents.MultipleObjectsReturned,
+ Exception) as ex:
+ logger.error(str(ex))
+ email_data = {
+ 'subject': "IncompletePaymentIntents error",
+ 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
+ 'to': settings.DCL_ERROR_EMAILS_TO_LIST,
+ 'body': "Response = %s" % str(ex),
+ }
+ send_plain_email_task.delay(email_data)
else:
logger.error("Unhandled event : " + event.type)
return HttpResponse(status=200)