+
+
+ {% trans "Data Center Light VM payment failed" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Data Center Light VM payment failed" %}
+
+
+
+
+
+{% blocktrans %}Your invoice payment for the VM {{VM_ID}} failed.
Please ensure that your credit card is active and that you have sufficient credit.{% endblocktrans %}
+
+{% blocktrans %}We will reattempt with your active payment source in the next {number_of_remaining_hours} hours. If this is not resolved by then, the VM and your subscription will be terminated and the VM can not be recovered back.{% endblocktrans %}
+
+{% blocktrans %}Please reply to this email or write to us at support@datacenterlight.ch if you have any queries.{% endblocktrans %}
+
+
+
+
+
+
{% trans "Your Data Center Light Team" %}
+
+
+
+
+
+
diff --git a/datacenterlight/templates/datacenterlight/emails/invoice_failed.txt b/datacenterlight/templates/datacenterlight/emails/invoice_failed.txt
new file mode 100644
index 00000000..f1b5b1d6
--- /dev/null
+++ b/datacenterlight/templates/datacenterlight/emails/invoice_failed.txt
@@ -0,0 +1,11 @@
+{% load i18n %}
+
+{% trans "Data Center Light VM payment failed" %}
+
+{% blocktrans %}Your invoice payment for the VM {{VM_ID}} failed.
Please ensure that your credit card is active and that you have sufficient credit.{% endblocktrans %}
+
+{% blocktrans %}We will reattempt with your active payment source in the next {number_of_remaining_hours} hours. If this is not resolved by then, the VM and your subscription will be terminated and the VM can not be recovered back.{% endblocktrans %}
+
+{% blocktrans %}Please reply to this email or write to us at support@datacenterlight.ch if you have any queries.{% endblocktrans %}
+
+{% trans "Your Data Center Light Team" %}
\ No newline at end of file
diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py
index ca1bb930..74798cc3 100644
--- a/datacenterlight/tests.py
+++ b/datacenterlight/tests.py
@@ -1,6 +1,7 @@
# from django.test import TestCase
-
+import datetime
from time import sleep
+from unittest import skipIf
import stripe
from celery.result import AsyncResult
@@ -8,7 +9,6 @@ from django.conf import settings
from django.core.management import call_command
from django.test import TestCase, override_settings
from model_mommy import mommy
-from unittest import skipIf
from datacenterlight.models import VMTemplate
from datacenterlight.tasks import create_vm_task
@@ -119,11 +119,14 @@ class CeleryTaskTestCase(TestCase):
subscription_result = self.stripe_utils.subscribe_customer_to_plan(
stripe_customer.stripe_id,
[{"plan": stripe_plan.get(
- 'response_object').stripe_plan_id}])
+ 'response_object').stripe_plan_id}],
+ int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_TRIAL_PERIOD_TO_SUBSCRIPTION else None)
stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active
- if stripe_subscription_obj is None \
- or stripe_subscription_obj.status != 'active':
+ if (stripe_subscription_obj is None or
+ (stripe_subscription_obj.status != 'active' and
+ stripe_subscription_obj.status != 'trialing')
+ ):
msg = subscription_result.get('error')
raise Exception("Creating subscription failed: {}".format(msg))
diff --git a/datacenterlight/views.py b/datacenterlight/views.py
index 952fa47f..53b5a6df 100644
--- a/datacenterlight/views.py
+++ b/datacenterlight/views.py
@@ -1,3 +1,4 @@
+import datetime
import logging
from django import forms
@@ -828,11 +829,15 @@ class OrderConfirmationView(DetailView, FormView):
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get(
- 'response_object').stripe_plan_id}])
+ 'response_object').stripe_plan_id}],
+ int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_TRIAL_PERIOD_TO_SUBSCRIPTION else None
+ )
stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active
- if (stripe_subscription_obj is None
- or stripe_subscription_obj.status != 'active'):
+ if (stripe_subscription_obj is None or
+ (stripe_subscription_obj.status != 'active' and
+ stripe_subscription_obj.status != 'trialing')
+ ):
# At this point, we have created a Stripe API card and
# associated it with the customer; but the transaction failed
# due to some reason. So, we would want to dissociate this card
diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py
index bdbb8f8f..9c89a8ff 100644
--- a/dynamicweb/settings/base.py
+++ b/dynamicweb/settings/base.py
@@ -153,6 +153,7 @@ INSTALLED_APPS = (
'rest_framework',
'opennebula_api',
'django_celery_results',
+ 'webhook',
)
MIDDLEWARE_CLASSES = (
@@ -650,6 +651,7 @@ CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5)
DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO')
ADMIN_EMAIL = env('ADMIN_EMAIL')
+WEBHOOK_EMAIL_TO = env('WEBHOOK_EMAIL_TO')
DCL_ERROR_EMAILS_TO_LIST = []
if DCL_ERROR_EMAILS_TO is not None:
@@ -720,7 +722,10 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else
X_FRAME_OPTIONS_ALLOW_FROM_URI.strip()
))
+INVOICE_WEBHOOK_SECRET = env('INVOICE_WEBHOOK_SECRET')
+
DEBUG = bool_env('DEBUG')
+ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION')
# LDAP setup
diff --git a/dynamicweb/urls.py b/dynamicweb/urls.py
index 37bb69a4..e07ca6bc 100644
--- a/dynamicweb/urls.py
+++ b/dynamicweb/urls.py
@@ -11,6 +11,7 @@ from hosting.views import (
RailsHostingView, DjangoHostingView, NodeJSHostingView
)
from datacenterlight.views import PaymentOrderView
+from webhook import views as webhook_views
from membership import urls as membership_urls
from ungleich_page.views import LandingView
from django.views.generic import RedirectView
@@ -62,6 +63,7 @@ urlpatterns += i18n_patterns(
name='blog_list_view'),
url(r'^cms/', include('cms.urls')),
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
+ url(r'^webhooks/invoices/', webhook_views.handle_invoice_webhook),
url(r'^$', RedirectView.as_view(url='/cms') if REDIRECT_TO_CMS
else LandingView.as_view()),
url(r'^', include('ungleich_page.urls', namespace='ungleich_page')),
diff --git a/hosting/migrations/0053_auto_20190415_1952.py b/hosting/migrations/0053_auto_20190415_1952.py
new file mode 100644
index 00000000..9a8a9c3d
--- /dev/null
+++ b/hosting/migrations/0053_auto_20190415_1952.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2019-04-15 19:52
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hosting', '0052_hostingbilllineitem'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='monthlyhostingbill',
+ name='order',
+ field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.HostingOrder'),
+ ),
+ ]
diff --git a/hosting/migrations/0056_merge.py b/hosting/migrations/0056_merge.py
new file mode 100644
index 00000000..af8acc73
--- /dev/null
+++ b/hosting/migrations/0056_merge.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2019-07-18 03:26
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hosting', '0055_auto_20190701_1614'),
+ ('hosting', '0053_auto_20190415_1952'),
+ ]
+
+ operations = [
+ ]
diff --git a/hosting/models.py b/hosting/models.py
index 0e6caa50..b858c4d2 100644
--- a/hosting/models.py
+++ b/hosting/models.py
@@ -1,4 +1,3 @@
-import decimal
import json
import logging
import os
@@ -254,7 +253,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
Corresponds to Invoice object of Stripe
"""
customer = models.ForeignKey(StripeCustomer)
- order = models.ForeignKey(HostingOrder)
+ order = models.ForeignKey(
+ HostingOrder, null=True, blank=True, default=None,
+ on_delete=models.SET_NULL
+ )
created = models.DateTimeField(help_text="When the invoice was created")
receipt_number = models.CharField(
help_text="The receipt number that is generated on Stripe",
@@ -542,6 +544,19 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model):
)
return item_detail
+ def get_vm_id(self):
+ """
+ If VM_ID is set in the metadata extract and return it as integer
+ other return None
+
+ :return:
+ """
+ if "VM_ID" in self.metadata:
+ data = json.loads(self.metadata)
+ return int(data["VM_ID"])
+ else:
+ return None
+
class VMDetail(models.Model):
user = models.ForeignKey(CustomUser)
@@ -724,4 +739,47 @@ class VATRates(AssignPermissionsMixin, models.Model):
currency_code = models.CharField(max_length=10)
rate = models.FloatField()
rate_type = models.TextField(blank=True, default='')
- description = models.TextField(blank=True, default='')
\ No newline at end of file
+ description = models.TextField(blank=True, default='')
+
+
+class FailedInvoice(AssignPermissionsMixin, models.Model):
+ permissions = ('view_failedinvoice',)
+ stripe_customer = models.ForeignKey(StripeCustomer)
+ order = models.ForeignKey(
+ HostingOrder, null=True, blank=True, default=None,
+ on_delete=models.SET_NULL
+ )
+ created_at = models.DateTimeField(auto_now_add=True)
+ number_of_attempts = models.IntegerField(
+ default=0,
+ help_text="The number of attempts for repayment")
+ invoice_id = models.CharField(
+ unique=True,
+ max_length=127,
+ help_text= "The ID of the invoice that failed")
+ result = models.IntegerField(
+ help_text="Whether the service was interrupted or another payment "
+ "succeeded"
+ )
+ service_interrupted_at = models.DateTimeField(
+ help_text="The datetime if/when service was interrupted"
+ )
+
+ class Meta:
+ permissions = (
+ ('view_failedinvoice', 'View Failed Invoice'),
+ )
+
+ @classmethod
+ def create(cls, stripe_customer=None, order=None, invoice_id=None,
+ number_of_attempts=0):
+ instance = cls.objects.create(
+ stripe_customer=stripe_customer,
+ order=order,
+ number_of_attempts=number_of_attempts,
+ invoice_id=invoice_id
+ )
+ instance.assign_permissions(stripe_customer.user)
+ return instance
+
+
diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html
index 5873a2aa..3281d9c3 100644
--- a/hosting/templates/hosting/virtual_machine_detail.html
+++ b/hosting/templates/hosting/virtual_machine_detail.html
@@ -46,7 +46,7 @@
{% trans "Current Pricing" %}
{{order.price|floatformat:2|intcomma}} CHF/{% if order.generic_product %}{% trans order.generic_product.product_subscription_interval %}{% else %}{% trans "Month" %}{% endif %}