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 1cd2eb05..44c4dee4 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,62 @@
+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
+ * Feature: Show Stripe invoices until we have a better way of showing them elegantly
+ * Bugfix: Fix bug where VAT is set to 0 because user set a valid VAT number before but later chose not to use any VAT number
+2.9.5: 2020-01-20
+ * Feature: Show invoices directly from stripe (MR!727)
+2.9.4: 2020-01-10
+ * Bugfix: Buying VPN generic item caused 500 error
2.9.3: 2020-01-05
* Feature: Add StripeTaxRate model to save tax rates created in Stripe
* Bugfix: Add vat rates for CY, IL and LI manually
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 ebb78a1c..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: 2019-12-09 12:13+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,9 +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"
@@ -407,6 +406,19 @@ msgstr "Datum"
msgid "Billed to"
msgstr "Rechnungsadresse"
+msgid "VAT Number"
+msgstr "MwSt-Nummer"
+
+msgid "Your VAT number has been verified"
+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."
+
msgid "Payment method"
msgstr "Bezahlmethode"
@@ -419,64 +431,63 @@ 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"
msgid "Recurring"
msgstr "Wiederholend"
-msgid "Subtotal"
-msgstr "Zwischensumme"
+msgid "Price Before VAT"
+msgstr "Preis ohne MwSt."
-#, fuzzy, python-format
-#| msgid ""
-#| "By clicking \"Place order\" this plan will charge your credit card "
-#| "account with %(total_price)s CHF/month"
-msgid ""
-"By clicking \"Place order\" 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"
+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/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/year"
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"
+"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 %(vm_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 "
-"%(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\" you agree to our Terms of Service and "
+"this plan will charge your credit card account with %(total_price)s CHF"
+msgstr ""
+"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"
@@ -575,6 +586,9 @@ msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
msgid "Actions speak louder than words. Let's do it, try our VM now."
msgstr "Taten sagen mehr als Worte – Teste jetzt unsere VM!"
+msgid "See Invoice"
+msgstr "Siehe Rechnung"
+
msgid "Invalid number of cores"
msgstr "Ungültige Anzahle CPU-Kerne"
@@ -592,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 ""
@@ -612,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."
@@ -655,15 +686,43 @@ 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"
#~ msgid "VAT"
#~ msgstr "Mehrwertsteuer"
@@ -676,9 +735,6 @@ msgstr ""
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
#~ "hast."
-#~ msgid "Card Number"
-#~ msgstr "Kreditkartennummer"
-
#~ msgid ""
#~ "You are not making any payment yet. After placing your order, you will be "
#~ "taken to the Submit Payment Page."
@@ -730,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/css/hosting.css b/datacenterlight/static/datacenterlight/css/hosting.css
index 0f16ab77..bcf266cc 100644
--- a/datacenterlight/static/datacenterlight/css/hosting.css
+++ b/datacenterlight/static/datacenterlight/css/hosting.css
@@ -532,6 +532,7 @@
.order-detail-container .total-price {
font-size: 18px;
+ line-height: 20px;
}
@media (max-width: 767px) {
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 8b4626e8..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,108 +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:orders',
- kwargs={'pk': order_id}),
- '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:
@@ -215,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 db3f73a8..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 %}
@@ -58,53 +66,121 @@
{% trans "Order summary" %}
+
{% if generic_payment_details %}
+
+
{% trans "Product" %}:
{{ generic_payment_details.product_name }}
-
-
- {% if generic_payment_details.vat_rate > 0 %}
-
- {% trans "Price" %}:
- CHF {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}}
-
+ {% trans "Your Price in Total" %}
+ {{vm.total_price|floatformat:2|intcomma}} CHF
+
{% endif %}
@@ -165,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 %}
@@ -216,5 +330,14 @@
{%endblock%}
diff --git a/datacenterlight/templatetags/custom_tags.py b/datacenterlight/templatetags/custom_tags.py
index a2b20bcb..120cabbf 100644
--- a/datacenterlight/templatetags/custom_tags.py
+++ b/datacenterlight/templatetags/custom_tags.py
@@ -1,6 +1,15 @@
+import datetime
+import logging
+
from django import template
from django.core.urlresolvers import resolve, reverse
-from django.utils.translation import activate, get_language
+from django.utils.safestring import mark_safe
+from django.utils.translation import activate, get_language, ugettext_lazy as _
+
+from hosting.models import GenericProduct, HostingOrder
+from utils.hosting_utils import get_ip_addresses
+
+logger = logging.getLogger(__name__)
register = template.Library()
@@ -52,3 +61,116 @@ def escaped_line_break(value):
:return:
"""
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 bd72af94..e2f6e13b 100644
--- a/hosting/views.py
+++ b/hosting/views.py
@@ -1,8 +1,10 @@
import logging
import uuid
from datetime import datetime
+from urllib.parse import quote
from time import sleep
+import stripe
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -10,7 +12,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.tokens import default_token_generator
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
)
@@ -40,8 +44,7 @@ from datacenterlight.models import VMTemplate, VMPricing
from datacenterlight.utils import (
create_vm, get_cms_integration, check_otp, validate_vat_number
)
-from dynamicweb.settings.base import DCL_ERROR_EMAILS_TO_LIST
-from hosting.models import UserCardDetail
+from hosting.models import UserCardDetail, StripeTaxRate
from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import (
@@ -57,11 +60,10 @@ from utils.hosting_utils import (
get_vm_price_with_vat, get_vm_price_for_given_vat, HostingUtils,
get_vat_rate_for_country
)
+from utils.ldap_manager import LdapManager
from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
-from utils.ldap_manager import LdapManager
-
from utils.views import (
PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin,
ResendActivationLinkViewMixin
@@ -73,7 +75,7 @@ from .forms import (
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
- GenericProduct, MonthlyHostingBill, HostingBillLineItem
+ GenericProduct, MonthlyHostingBill
)
logger = logging.getLogger(__name__)
@@ -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:
@@ -1135,7 +1133,8 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
- amount_to_be_charged = specs.get('total_price')
+ amount_to_be_charged = specs.get('price')
+ discount = specs.get('discount')
plan_name = StripeUtils.get_stripe_plan_name(
cpu=cpu,
memory=memory,
@@ -1154,11 +1153,61 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
amount=amount_to_be_charged,
name=plan_name,
stripe_plan_id=stripe_plan_id)
+ # Create StripeTaxRate if applicable to the user
+ stripe_tax_rate = None
+ if specs["vat_percent"] > 0:
+ try:
+ stripe_tax_rate = StripeTaxRate.objects.get(
+ description="VAT for %s" % specs["vat_country"]
+ )
+ print("Stripe Tax Rate exists")
+ except StripeTaxRate.DoesNotExist as dne:
+ print("StripeTaxRate does not exist")
+ tax_rate_obj = stripe.TaxRate.create(
+ display_name="VAT",
+ description="VAT for %s" % specs["vat_country"],
+ jurisdiction=specs["vat_country"],
+ percentage=specs["vat_percent"] * 100,
+ inclusive=False,
+ )
+ stripe_tax_rate = StripeTaxRate.objects.create(
+ display_name=tax_rate_obj.display_name,
+ description=tax_rate_obj.description,
+ jurisdiction=tax_rate_obj.jurisdiction,
+ percentage=tax_rate_obj.percentage,
+ inclusive=False,
+ tax_rate_id=tax_rate_obj.id
+ )
+ logger.debug("Created StripeTaxRate %s" %
+ stripe_tax_rate.tax_rate_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
- [{"plan": stripe_plan.get(
- 'response_object').stripe_plan_id}])
+ [{"plan": stripe_plan.get('response_object').stripe_plan_id}],
+ coupon=(discount['stripe_coupon_id']
+ if 'name' in discount and
+ discount['name'] is not None and
+ 'ipv6' in discount['name'].lower() and
+ discount['stripe_coupon_id']
+ else ""),
+ tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [],
+ default_payment_method=request.session['id_payment_method']
+ )
stripe_subscription_obj = subscription_result.get('response_object')
+ 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'):
@@ -1197,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(),
@@ -1240,7 +1290,7 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
return super(OrdersHostingListView, self).get(request, *args, **kwargs)
-class InvoiceListView(LoginRequiredMixin, ListView):
+class InvoiceListView(LoginRequiredMixin, TemplateView):
template_name = "hosting/invoices.html"
login_url = reverse_lazy('hosting:login')
context_object_name = "invoices"
@@ -1248,10 +1298,14 @@ class InvoiceListView(LoginRequiredMixin, ListView):
ordering = '-created'
def get_context_data(self, **kwargs):
+ 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'] = '%s' % quote(user_email)
logger.debug(
"user_email = {}".format(user_email)
)
@@ -1260,53 +1314,66 @@ class InvoiceListView(LoginRequiredMixin, ListView):
except CustomUser.DoesNotExist as dne:
logger.debug("User does not exist")
cu = self.request.user
- mhbs = MonthlyHostingBill.objects.filter(customer__user=cu)
- else:
- mhbs = MonthlyHostingBill.objects.filter(
- customer__user=self.request.user
- )
- ips_dict = {}
- line_item_period_dict = {}
- for mhb in mhbs:
+ invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id,
+ count=100,
+ status='paid')
+ paginator = Paginator(invs.data, 10)
try:
- vm_detail = VMDetail.objects.get(vm_id=mhb.order.vm_id)
- ips_dict[mhb.invoice_number] = [vm_detail.ipv6, vm_detail.ipv4]
- all_line_items = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb)
- for line_item in all_line_items:
- if line_item.get_item_detail_str() != "":
- line_item_period_dict[mhb.invoice_number] = {
- "period_start": line_item.period_start,
- "period_end": line_item.period_end
- }
- break
- except VMDetail.DoesNotExist as dne:
- ips_dict[mhb.invoice_number] = ['--']
- logger.debug("VMDetail for {} doesn't exist".format(
- mhb.order.vm_id
- ))
- context['ips'] = ips_dict
- context['period'] = line_item_period_dict
- return context
+ invs_page = paginator.page(page)
+ except PageNotAnInteger:
+ 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)})
- def get_queryset(self):
- user = self.request.user
- if ('user_email' in self.request.GET
- and self.request.user.email == settings.ADMIN_EMAIL):
- user_email = self.request.GET['user_email']
- logger.debug(
- "user_email = {}".format(user_email)
- )
+ paginator_charges = Paginator(stripe_chgs, 10)
try:
- cu = CustomUser.objects.get(email=user_email)
- except CustomUser.DoesNotExist as dne:
- logger.debug("User does not exist")
- cu = self.request.user
- self.queryset = MonthlyHostingBill.objects.filter(customer__user=cu)
+ 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:
- self.queryset = MonthlyHostingBill.objects.filter(
- customer__user=self.request.user
- )
- return super(InvoiceListView, self).get_queryset()
+ try:
+ invs = stripe.Invoice.list(
+ customer=self.request.user.stripecustomer.stripe_id,
+ count=100
+ )
+ paginator = Paginator(invs.data, 10)
+ try:
+ invs_page = paginator.page(page)
+ except PageNotAnInteger:
+ 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)
def get(self, request, *args, **kwargs):
@@ -1386,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)
@@ -1469,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()
@@ -1630,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')
@@ -1674,13 +1741,28 @@ class VirtualMachineView(LoginRequiredMixin, View):
context = None
try:
serializer = VirtualMachineSerializer(vm)
+ hosting_order = HostingOrder.objects.get(
+ vm_id=serializer.data['vm_id']
+ )
+ inv_url = None
+ if hosting_order.subscription_id:
+ stripe_obj = stripe.Invoice.list(
+ subscription=hosting_order.subscription_id,
+ count=1
+ )
+ inv_url = stripe_obj.data[0].hosted_invoice_url
+ elif hosting_order.stripe_charge_id:
+ stripe_obj = stripe.Charge.retrieve(
+ hosting_order.stripe_charge_id
+ )
+ inv_url = stripe_obj.receipt_url
context = {
'virtual_machine': serializer.data,
- 'order': HostingOrder.objects.get(
- vm_id=serializer.data['vm_id']
- ),
- 'keys': UserHostingKey.objects.filter(user=request.user)
+ 'order': hosting_order,
+ 'keys': UserHostingKey.objects.filter(user=request.user),
+ 'inv_url': inv_url
}
+
except Exception as ex:
logger.debug("Exception generated {}".format(str(ex)))
messages.error(self.request,
@@ -1699,7 +1781,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
vm = self.get_object()
manager = OpenNebulaManager(
- email=owner.email,
+ email=owner.username,
password=owner.password
)
try:
@@ -1727,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
@@ -1749,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:
@@ -1772,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. "
@@ -1799,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
@@ -1816,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()]),
}
@@ -1864,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 73e2c035..b9e2eb8a 100644
--- a/utils/hosting_utils.py
+++ b/utils/hosting_utils.py
@@ -1,7 +1,10 @@
import decimal
import logging
+import math
import subprocess
+from django.conf import settings
+
from oca.pool import WrongIdError
from datacenterlight.models import VMPricing
@@ -78,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)
@@ -101,18 +105,25 @@ 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
+ discount_amount = round(float(pricing.discount_amount), 2)
vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01)
vat_percent = vat_rate
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
+ discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01))
+ discount_amount_with_vat = discount_amount_with_vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
- 'name': pricing.discount_name,
- 'amount': round(float(pricing.discount_amount), 2)
+ 'name': discount_name,
+ 'amount': discount_amount,
+ '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)
@@ -148,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)
@@ -162,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)
@@ -199,6 +212,16 @@ def get_vat_rate_for_country(country):
return 0
+def get_ip_addresses(vm_id):
+ try:
+ vm_detail = VMDetail.objects.get(vm_id=vm_id)
+ return "%s %s" % (vm_detail.ipv6, vm_detail.ipv4)
+ except VMDetail.DoesNotExist as dne:
+ logger.error(str(dne))
+ logger.error("VMDetail for %s does not exist" % vm_id)
+ return "--"
+
+
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 de16fe4b..875a174e 100644
--- a/utils/stripe_utils.py
+++ b/utils/stripe_utils.py
@@ -1,7 +1,9 @@
import logging
import re
+
import stripe
from django.conf import settings
+
from datacenterlight.models import StripePlan
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
@@ -32,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
@@ -67,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.'
@@ -80,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):
@@ -185,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)
@@ -204,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
)
@@ -265,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
@@ -297,10 +335,15 @@ class StripeUtils(object):
return return_value
@handleStripeError
- def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
+ def subscribe_customer_to_plan(self, customer, plans, trial_end=None,
+ 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
@@ -314,10 +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
+ 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
@@ -348,7 +398,7 @@ class StripeUtils(object):
@staticmethod
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
- price=None):
+ price=None, excl_vat=True):
"""
Returns the Stripe plan id string of the form
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
@@ -375,13 +425,16 @@ class StripeUtils(object):
plan=dcl_plan_string
)
if price is not None:
- stripe_plan_id_string_with_price = '{}-{}chf'.format(
+ stripe_plan_id_string = '{}-{}chf'.format(
stripe_plan_id_string,
round(price, 2)
)
- return stripe_plan_id_string_with_price
- else:
- return stripe_plan_id_string
+ if excl_vat:
+ stripe_plan_id_string = '{}-{}'.format(
+ stripe_plan_id_string,
+ "excl_vat"
+ )
+ return stripe_plan_id_string
@staticmethod
def get_vm_config_from_stripe_id(stripe_id):
@@ -410,18 +463,27 @@ class StripeUtils(object):
@staticmethod
- def get_stripe_plan_name(cpu, memory, disk_size, price):
+ def get_stripe_plan_name(cpu, memory, disk_size, price, excl_vat=True):
"""
Returns the Stripe plan name
:return:
"""
- return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
- "{price} CHF".format(
- cpu=cpu,
- memory=memory,
- disk_size=disk_size,
- price=round(price, 2)
- )
+ if excl_vat:
+ return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
+ "{price} CHF Excl. VAT".format(
+ cpu=cpu,
+ memory=memory,
+ disk_size=disk_size,
+ price=round(price, 2)
+ )
+ else:
+ return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
+ "{price} CHF".format(
+ cpu=cpu,
+ memory=memory,
+ disk_size=disk_size,
+ price=round(price, 2)
+ )
@handleStripeError
def set_subscription_meta_data(self, subscription_id, meta_data):
@@ -463,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)