diff --git a/datacenterlight/templates/datacenterlight/emails/invoice_failed.html b/datacenterlight/templates/datacenterlight/emails/invoice_failed.html new file mode 100644 index 00000000..7f973c88 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/emails/invoice_failed.html @@ -0,0 +1,44 @@ +{% load static i18n %} + + + + + + + {% 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 %}
- {% trans "See Invoice" %} + {% trans "See Invoice" %}
diff --git a/hosting/views.py b/hosting/views.py index 83995ac4..54cdc481 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1094,11 +1094,14 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): subscription_result = stripe_utils.subscribe_customer_to_plan( stripe_api_cus_id, [{"plan": stripe_plan.get( - 'response_object').stripe_plan_id}]) + 'response_object').stripe_plan_id}], + int(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'): + (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 @@ -1259,6 +1262,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): def get_object(self, queryset=None): invoice_id = self.kwargs.get('invoice_id') + logger.debug("Getting invoice for %s" % invoice_id) try: invoice_obj = MonthlyHostingBill.objects.get( invoice_number=invoice_id @@ -1287,47 +1291,22 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): if obj is not None: vm_id = obj.get_vm_id() - try: - # Try to get vm details from database - vm_detail = VMDetail.objects.get(vm_id=vm_id) - context['vm'] = vm_detail.__dict__ - context['vm']['name'] = '{}-{}'.format( - context['vm']['configuration'], context['vm']['vm_id']) - user_vat_country = obj.order.billing_address.country - user_country_vat_rate = get_vat_rate_for_country( - user_vat_country) - price, vat, vat_percent, discount = get_vm_price_for_given_vat( - cpu=context['vm']['cores'], - ssd_size=context['vm']['disk_size'], - memory=context['vm']['memory'], - pricing_name=(obj.order.vm_pricing.name - if obj.order.vm_pricing else 'default'), - vat_rate=( - user_country_vat_rate * 100 - if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT - else settings.PRE_EU_VAT_RATE - ) - ) - context['vm']["after_eu_vat_intro"] = ( - True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT - else False - ) - context['vm']["price"] = price - context['vm']["vat"] = vat - context['vm']["vat_percent"] = vat_percent - context['vm']["vat_country"] = user_vat_country - context['vm']["discount"] = discount - context['vm']["total_price"] = round( - price + vat - discount['amount'], 2) - except VMDetail.DoesNotExist: - # fallback to get it from the infrastructure + if vm_id is None: + # We did not find it in the metadata, fallback to order + if obj.order is not None: + vm_id = obj.order.vm_id + logger.debug("VM ID from order is %s" % vm_id) + else: + logger.debug("VM order is None. So, we don't have VM_ID") + else: + logger.debug("VM ID was set in metadata") + if vm_id > 0: try: - manager = OpenNebulaManager( - email=self.request.user.email, - password=self.request.user.password - ) - vm = manager.get_vm(vm_id) - context['vm'] = VirtualMachineSerializer(vm).data + # Try to get vm details from database + vm_detail = VMDetail.objects.get(vm_id=vm_id) + context['vm'] = vm_detail.__dict__ + context['vm']['name'] = '{}-{}'.format( + context['vm']['configuration'], context['vm']['vm_id']) user_vat_country = obj.order.billing_address.country user_country_vat_rate = get_vat_rate_for_country( user_vat_country) @@ -1354,21 +1333,58 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): context['vm']["discount"] = discount context['vm']["total_price"] = round( price + vat - discount['amount'], 2) - except TypeError: - logger.error("Type error. Probably we " - "came from a generic product. " - "Invoice ID %s" % obj.invoice_id) - except WrongIdError: - logger.error("WrongIdError while accessing " - "invoice {}".format(obj.invoice_id)) - messages.error( - self.request, - _('The VM you are looking for is unavailable at the ' - 'moment. Please contact Data Center Light support.') - ) - self.kwargs['error'] = 'WrongIdError' - context['error'] = 'WrongIdError' - return context + except VMDetail.DoesNotExist: + # fallback to get it from the infrastructure + try: + manager = OpenNebulaManager( + email=self.request.user.email, + password=self.request.user.password + ) + vm = manager.get_vm(vm_id) + context['vm'] = VirtualMachineSerializer(vm).data + user_vat_country = obj.order.billing_address.country + user_country_vat_rate = get_vat_rate_for_country( + user_vat_country) + price, vat, vat_percent, discount = get_vm_price_for_given_vat( + cpu=context['vm']['cores'], + ssd_size=context['vm']['disk_size'], + memory=context['vm']['memory'], + pricing_name=(obj.order.vm_pricing.name + if obj.order.vm_pricing else 'default'), + vat_rate=( + user_country_vat_rate * 100 + if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT + else settings.PRE_EU_VAT_RATE + ) + ) + context['vm']["after_eu_vat_intro"] = ( + True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT + else False + ) + context['vm']["price"] = price + context['vm']["vat"] = vat + context['vm']["vat_percent"] = vat_percent + context['vm']["vat_country"] = user_vat_country + context['vm']["discount"] = discount + context['vm']["total_price"] = round( + price + vat - discount['amount'], 2) + except TypeError: + logger.error("Type error. Probably we " + "came from a generic product. " + "Invoice ID %s" % obj.invoice_id) + except WrongIdError: + logger.error("WrongIdError while accessing " + "invoice {}".format(obj.invoice_id)) + messages.error( + self.request, + _('The VM you are looking for is unavailable at the ' + 'moment. Please contact Data Center Light support.') + ) + self.kwargs['error'] = 'WrongIdError' + context['error'] = 'WrongIdError' + return context + else: + logger.debug("No VM_ID. So, no details available.") # add context params from monthly hosting bill context['period_start'] = obj.get_period_start() @@ -1616,8 +1632,21 @@ class VirtualMachineView(LoginRequiredMixin, View): 'order': HostingOrder.objects.get( vm_id=serializer.data['vm_id'] ), - 'keys': UserHostingKey.objects.filter(user=request.user) + 'keys': UserHostingKey.objects.filter(user=request.user), + 'has_invoices': False } + try: + bills = [] + if hasattr(self.request.user, 'stripecustomer'): + bills = MonthlyHostingBill.objects.filter( + customer=self.request.user.stripecustomer + ) + if len(bills) > 0: + context['has_invoices'] = True + except MonthlyHostingBill.DoesNotExist as dne: + logger.error("{}'s monthly hosting bill not imported ?".format( + self.request.user.email + )) except Exception as ex: logger.debug("Exception generated {}".format(str(ex))) messages.error(self.request, diff --git a/requirements.txt b/requirements.txt index 5fb2ec67..874fc1a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -79,7 +79,7 @@ requests==2.10.0 rjsmin==1.0.12 six==1.10.0 sqlparse==0.1.19 -stripe==1.33.0 +stripe==2.24.1 wheel==0.29.0 django-admin-honeypot==1.0.0 coverage==4.3.4 diff --git a/utils/tests.py b/utils/tests.py index 8abbbb1d..fb499a94 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -1,5 +1,7 @@ +import datetime import uuid from time import sleep +from unittest import skipIf from unittest.mock import patch import stripe @@ -8,7 +10,6 @@ from django.conf import settings from django.http.request import HttpRequest from django.test import Client from django.test import TestCase, override_settings -from unittest import skipIf from model_mommy import mommy from datacenterlight.models import StripePlan @@ -231,7 +232,8 @@ class StripePlanTestCase(TestStripeCustomerDescription): 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) self.assertIsInstance(result.get('response_object'), stripe.Subscription) self.assertIsNone(result.get('error')) @@ -247,7 +249,8 @@ class StripePlanTestCase(TestStripeCustomerDescription): 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) self.assertIsNone(result.get('response_object'), None) self.assertIsNotNone(result.get('error')) diff --git a/webhook/__init__.py b/webhook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/webhook/management/commands/webhook.py b/webhook/management/commands/webhook.py new file mode 100644 index 00000000..3648fb85 --- /dev/null +++ b/webhook/management/commands/webhook.py @@ -0,0 +1,81 @@ +import logging +import stripe + +from django.core.management.base import BaseCommand + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = '''creates webhook with the supplied arguments and returns the + webhook secret + ''' + + def add_arguments(self, parser): + parser.add_argument( + '--webhook_endpoint', + help="The url of the webhook endpoint that accepts the events " + "from stripe", + dest="webhook_endpoint", + required=False + ) + parser.add_argument('--events_csv', dest="events_csv", required=False) + parser.add_argument('--webhook_id', dest="webhook_id", required=False) + parser.add_argument('--create', dest='create', action='store_true') + parser.add_argument('--list', dest='list', action='store_true') + parser.add_argument('--delete', dest='delete', action='store_true') + + def handle(self, *args, **options): + wep_exists = False + if options['list']: + logger.debug("Listing webhooks") + we_list = stripe.WebhookEndpoint.list(limit=100) + for wep in we_list.data: + msg = wep.id + " -- " + ",".join(wep.enabled_events) + logger.debug(msg) + self.stdout.write( + self.style.SUCCESS(msg) + ) + elif options['delete']: + logger.debug("Deleting webhook") + if 'webhook_id' in options: + stripe.WebhookEndpoint.delete(options['webhook_id']) + msg = "Deleted " + options['webhook_id'] + logger.debug(msg) + self.stdout.write( + self.style.SUCCESS(msg) + ) + else: + msg = "Supply webhook_id to delete a webhook" + logger.debug(msg) + self.stdout.write( + self.style.SUCCESS(msg) + ) + exit(0) + elif options['create']: + logger.debug("Creating webhook") + try: + we_list = stripe.WebhookEndpoint.list(limit=100) + for wep in we_list.data: + if set(wep.enabled_events) == set(options['events_csv'].split(",")): + if wep.url == options['webhook_endpoint']: + logger.debug("We have this webhook already") + wep_exists = True + break + if wep_exists is False: + logger.debug( + "No webhook exists for {} at {}. Creatting a new endpoint " + "now".format( + options['webhook_endpoint'], options['events_csv'] + ) + ) + wep = stripe.WebhookEndpoint.create( + url=options['webhook_endpoint'], + enabled_events=options['events_csv'].split(",") + ) + self.stdout.write( + self.style.SUCCESS('Creation successful. ' + 'webhook_secret = %s' % wep.secret) + ) + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/webhook/models.py b/webhook/models.py new file mode 100644 index 00000000..d49766e4 --- /dev/null +++ b/webhook/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. \ No newline at end of file diff --git a/webhook/views.py b/webhook/views.py new file mode 100644 index 00000000..d20b266c --- /dev/null +++ b/webhook/views.py @@ -0,0 +1,172 @@ +import logging + +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 hosting.models import ( + MonthlyHostingBill, HostingBillLineItem, FailedInvoice +) +from membership.models import StripeCustomer +from utils.mailer import BaseEmail +from utils.tasks import send_plain_email_task + +logger = logging.getLogger(__name__) + + +@require_POST +@csrf_exempt +def handle_invoice_webhook(request): + payload = request.body + event = None + + try: + if 'HTTP_STRIPE_SIGNATURE' in request.META: + sig_header = request.META['HTTP_STRIPE_SIGNATURE'] + else: + logger.error("No HTTP_STRIPE_SIGNATURE header") + # Invalid payload + return HttpResponse(status=400) + event = stripe.Webhook.construct_event( + payload, sig_header, settings.INVOICE_WEBHOOK_SECRET + ) + except ValueError as e: + # Invalid payload + err_msg = "FAILURE handle_invoice_webhook: Invalid payload details" + err_body = "Details %s" % str(e) + return handle_error(err_msg, err_body) + except stripe.error.SignatureVerificationError as e: + # Invalid signature + err_msg = "FAILURE handle_invoice_webhook: SignatureVerificationError" + err_body = "Details %s" % str(e) + return handle_error(err_msg, err_body) + + # Do something with event + logger.debug("Passed invoice signature verification") + + # Get the user from the invoice + invoice = event.data.object + logger.debug("Checking whether StripeCustomer %s exists" % invoice.customer) + try: + stripe_customer = StripeCustomer.objects.get(stripe_id=invoice.customer) + except StripeCustomer.DoesNotExist as dne: + # StripeCustomer does not exist + err_msg = "FAILURE handle_invoice_webhook: StripeCustomer %s doesn't exist" % invoice.customer + err_body = "Details %s" % str(dne) + return handle_error(err_msg, err_body) + + if event.type == "invoice.payment_succeeded": + logger.debug("Invoice payment succeeded") + + # Create a new invoice for the user + invoice_dict = { + 'created': invoice.created, + 'receipt_number': invoice.receipt_number, + 'invoice_number': invoice.number, + 'paid_at': invoice.status_transitions.paid_at if invoice.paid else 0, + 'period_start': invoice.period_start, + 'period_end': invoice.period_end, + 'billing_reason': invoice.billing_reason, + 'discount': invoice.discount.coupon.amount_off if invoice.discount else 0, + 'total': invoice.total, + # to see how many line items we have in this invoice and + # then later check if we have more than 1 + 'lines_data_count': len( + invoice.lines.data) if invoice.lines.data is not None else 0, + 'invoice_id': invoice.id, + 'lines_meta_data_csv': ','.join( + [line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' + for line in invoice.lines.data] + ), + 'subscription_ids_csv': ','.join( + [line.id if line.type == 'subscription' else '' for line in + invoice.lines.data] + ), + 'line_items': invoice.lines.data, + 'customer': stripe_customer + } + mhb = MonthlyHostingBill.create(invoice_dict) + mbli = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb).first() + vm_id = mbli.get_vm_id() + + if vm_id is None: + vm_id = mhb.order.vm_id + + # Send an email to admin + admin_msg_sub = "Invoice payment success for user {} and VM {}".format( + stripe_customer.user.email, + vm_id if vm_id is not None else "Unknown" + ) + email_to_admin_data = { + 'subject': admin_msg_sub, + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': settings.WEBHOOK_EMAIL_TO.split(","), + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in invoice_dict.items()]), + } + logger.debug("Sending msg %s to %s" % (admin_msg_sub, + settings.WEBHOOK_EMAIL_TO)) + send_plain_email_task.delay(email_to_admin_data) + + elif event.type == "invoice.payment_failed": + # Create a failed invoice, so that we have a trace of which invoices + # need a followup + FailedInvoice.create( + stripe_customer, number_of_attempts = 1, invoice_id=invoice.id + ) + VM_ID = invoice.lines.data[0].metadata["VM_ID"] + admin_msg_sub = "Invoice payment FAILED for user {} and {}".format( + stripe_customer.user.email, + VM_ID + ) + logger.error(admin_msg_sub) + # send email to admin + email_to_admin_data = { + 'subject': admin_msg_sub, + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': settings.WEBHOOK_EMAIL_TO.split(","), + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in invoice.__dict__.items()]), + } + # send email to user + context = { + 'base_url': "{0}://{1}".format(request.scheme, + request.get_host()), + 'dcl_text': settings.DCL_TEXT, + 'VM_ID': VM_ID, + 'number_of_remaining_hours': 48 # for the first failure we wait 48 hours + } + email_data = { + 'subject': 'IMPORTANT: The payment for VM {VM_ID} at {dcl_text} failed'.format( + dcl_text=settings.DCL_TEXT, + VM_ID=invoice.lines.data.metadata + ), + 'to': stripe_customer.user.email, + 'context': context, + 'template_name': 'invoice_failed', + 'template_path': 'datacenterlight/emails/', + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS + } + email = BaseEmail(**email_data) + email.send() + + send_plain_email_task.delay(email_to_admin_data) + else: + logger.error("Unhandled event : " + event.type) + + return HttpResponse(status=200) + + +def handle_error(error_msg, error_body): + logger.error("%s -- %s" % (error_msg, error_body)) + email_to_admin_data = { + 'subject': error_msg, + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': [settings.ADMIN_EMAIL], + 'body': error_body, + } + send_plain_email_task.delay(email_to_admin_data) + return HttpResponse(status=400) \ No newline at end of file