From 68538ac981e162f1ded834dc9b4e89a2729abe89 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sat, 13 Apr 2019 22:20:09 +0200 Subject: [PATCH 01/30] Update stripe version (supports stripe.Webhook) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fe70299b..00dd0ad2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -78,7 +78,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 From 9b32290964c9e176bf0054e60653e5244522f4e0 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sat, 13 Apr 2019 23:52:41 +0200 Subject: [PATCH 02/30] Add webhook app and create_webhook management command --- dynamicweb/settings/base.py | 3 ++ webhook/__init__.py | 0 webhook/management/commands/create_webhook.py | 49 +++++++++++++++++++ webhook/models.py | 3 ++ webhook/views.py | 36 ++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 webhook/__init__.py create mode 100644 webhook/management/commands/create_webhook.py create mode 100644 webhook/models.py create mode 100644 webhook/views.py diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index a770018f..45018f13 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 = ( @@ -719,6 +720,8 @@ 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') if DEBUG: diff --git a/webhook/__init__.py b/webhook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/webhook/management/commands/create_webhook.py b/webhook/management/commands/create_webhook.py new file mode 100644 index 00000000..23960297 --- /dev/null +++ b/webhook/management/commands/create_webhook.py @@ -0,0 +1,49 @@ +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" + ) + parser.add_argument('--events_csv', dest="events_csv") + + def handle(self, *args, **options): + wep_exists = False + 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..19587e69 --- /dev/null +++ b/webhook/views.py @@ -0,0 +1,36 @@ +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 + +logger = logging.getLogger(__name__) + + +@require_POST +@csrf_exempt +def handle_invoice_webhook(request): + payload = request.body + sig_header = request.META['HTTP_STRIPE_SIGNATURE'] + event = None + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, settings.INVOICE_WEBHOOK_SECRET + ) + except ValueError as e: + logger.error("Invalid payload details = " + str(e)) + # Invalid payload + return HttpResponse(status=400) + except stripe.error.SignatureVerificationError as e: + logger.error("SignatureVerificationError details = " + str(e)) + # Invalid signature + return HttpResponse(status=400) + + # Do something with event + logger.debug("Passed invoice signature verification") + + return HttpResponse(status=200) \ No newline at end of file From 8a59c2da1e04fb216e8ab4993bf5f3a17bd368b0 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 00:58:13 +0200 Subject: [PATCH 03/30] Implement handling invoice.payment_succeeded and invoice.payment_failed webhooks --- dynamicweb/settings/base.py | 1 + hosting/models.py | 13 +++++++ webhook/views.py | 71 +++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 45018f13..85cf1a86 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -650,6 +650,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: diff --git a/hosting/models.py b/hosting/models.py index 550cf27f..ed6329b8 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -435,6 +435,19 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model): ('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'), ) + def get_vm_id(self): + """ + If VM_ID is set in the metadata extract and return it as integer + other return -1 + + :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) diff --git a/webhook/views.py b/webhook/views.py index 19587e69..cf1b0950 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -7,6 +7,9 @@ from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST +from membership.models import StripeCustomer +from hosting.models import MonthlyHostingBill, HostingBillLineItem +from utils.tasks import send_plain_email_task logger = logging.getLogger(__name__) @@ -33,4 +36,72 @@ def handle_invoice_webhook(request): # Do something with event logger.debug("Passed invoice signature verification") + # Get the user from the invoice + invoice = event.data.object + stripe_customer = StripeCustomer.objects.get(stripe_id=invoice.customer) + + 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() + + # 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()]), + } + send_plain_email_task.delay(email_to_admin_data) + + elif event.type == "invoice.payment_failed": + logger.error("Invoice payment failed") + + admin_msg_sub = "Invoice payment FAILED for user {} and ".format( + stripe_customer.user.email, + invoice.lines.data.metadata + ) + 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_plain_email_task.delay(email_to_admin_data) + return HttpResponse(status=200) \ No newline at end of file From 0ed3c84461777750756368b32c4e4b85ed0a4113 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 00:59:25 +0200 Subject: [PATCH 04/30] Log unhandled case --- webhook/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webhook/views.py b/webhook/views.py index cf1b0950..842f55b5 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -103,5 +103,7 @@ def handle_invoice_webhook(request): ["%s=%s" % (k, v) for (k, v) in invoice.__dict__.items()]), } send_plain_email_task.delay(email_to_admin_data) + else: + logger.error("Unhandled event : " + event.type) return HttpResponse(status=200) \ No newline at end of file From d71bf8747069fad50a6cd233e52a28eed14375d3 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 01:09:53 +0200 Subject: [PATCH 05/30] Add webhooks url --- dynamicweb/urls.py | 2 ++ 1 file changed, 2 insertions(+) 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')), From 3b84d6f646bd6dc7f81340c5db063608fc3a146e Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 01:36:03 +0200 Subject: [PATCH 06/30] Handle presence of HTTP_STRIPE_SIGNATURE header in META --- webhook/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webhook/views.py b/webhook/views.py index 842f55b5..84e53724 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -17,10 +17,15 @@ logger = logging.getLogger(__name__) @csrf_exempt def handle_invoice_webhook(request): payload = request.body - sig_header = request.META['HTTP_STRIPE_SIGNATURE'] 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 ) From dc4ad93de811bd37b013b06fee92c0b07d7071ee Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 01:59:29 +0200 Subject: [PATCH 07/30] Add list/delete functionality to webhook --- webhook/management/commands/create_webhook.py | 49 ----------- webhook/management/commands/webhook.py | 81 +++++++++++++++++++ 2 files changed, 81 insertions(+), 49 deletions(-) delete mode 100644 webhook/management/commands/create_webhook.py create mode 100644 webhook/management/commands/webhook.py diff --git a/webhook/management/commands/create_webhook.py b/webhook/management/commands/create_webhook.py deleted file mode 100644 index 23960297..00000000 --- a/webhook/management/commands/create_webhook.py +++ /dev/null @@ -1,49 +0,0 @@ -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" - ) - parser.add_argument('--events_csv', dest="events_csv") - - def handle(self, *args, **options): - wep_exists = False - 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/management/commands/webhook.py b/webhook/management/commands/webhook.py new file mode 100644 index 00000000..b5fd3184 --- /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.Webhook.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))) From 5cb1c136cf57130b9914aa68cb7ff680fecb5e3c Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 02:03:06 +0200 Subject: [PATCH 08/30] Fex checking webhook_id in options --- webhook/management/commands/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook/management/commands/webhook.py b/webhook/management/commands/webhook.py index b5fd3184..8e17d5e1 100644 --- a/webhook/management/commands/webhook.py +++ b/webhook/management/commands/webhook.py @@ -38,7 +38,7 @@ class Command(BaseCommand): ) elif options['delete']: logger.debug("Deleting webhook") - if ['webhook_id'] in options: + if 'webhook_id' in options: stripe.Webhook.delete(options['webhook_id']) msg = "Deleted " + options['webhook_id'] logger.debug(msg) From e7196af1f96732e1eb6b87f3503c0fcc1b398997 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 02:04:12 +0200 Subject: [PATCH 09/30] Fix a typo --- webhook/management/commands/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook/management/commands/webhook.py b/webhook/management/commands/webhook.py index 8e17d5e1..3648fb85 100644 --- a/webhook/management/commands/webhook.py +++ b/webhook/management/commands/webhook.py @@ -39,7 +39,7 @@ class Command(BaseCommand): elif options['delete']: logger.debug("Deleting webhook") if 'webhook_id' in options: - stripe.Webhook.delete(options['webhook_id']) + stripe.WebhookEndpoint.delete(options['webhook_id']) msg = "Deleted " + options['webhook_id'] logger.debug(msg) self.stdout.write( From c081f9e73a8ede6655e8dc0b4a8ea47e069658ae Mon Sep 17 00:00:00 2001 From: PCoder Date: Sun, 14 Apr 2019 11:17:48 +0200 Subject: [PATCH 10/30] Handle error better --- webhook/views.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/webhook/views.py b/webhook/views.py index 84e53724..8b8a65b0 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -30,20 +30,29 @@ def handle_invoice_webhook(request): payload, sig_header, settings.INVOICE_WEBHOOK_SECRET ) except ValueError as e: - logger.error("Invalid payload details = " + str(e)) # Invalid payload - return HttpResponse(status=400) + 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: - logger.error("SignatureVerificationError details = " + str(e)) # Invalid signature - return HttpResponse(status=400) + 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 - stripe_customer = StripeCustomer.objects.get(stripe_id=invoice.customer) + 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") @@ -111,4 +120,16 @@ def handle_invoice_webhook(request): else: logger.error("Unhandled event : " + event.type) - return HttpResponse(status=200) \ No newline at end of file + 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.WEBHOOK_EMAIL_TO.split(","), + 'body': error_body, + } + send_plain_email_task.delay(email_to_admin_data) + return HttpResponse(status=400) \ No newline at end of file From eb360c74063ba973593b36a056c1e89af10e0b77 Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 15 Apr 2019 21:53:35 +0200 Subject: [PATCH 11/30] Make HostingOrder not mandatory in MonthlyHostingBill --- hosting/migrations/0053_auto_20190415_1952.py | 21 +++++++++++++++++++ hosting/models.py | 17 +++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 hosting/migrations/0053_auto_20190415_1952.py 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/models.py b/hosting/models.py index ed6329b8..12ff687c 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -240,7 +240,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", @@ -274,9 +277,15 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): if len(args['subscription_ids_csv']) > 0: sub_ids = [sub_id.strip() for sub_id in args['subscription_ids_csv'].split(",")] if len(sub_ids) == 1: - args['order'] = HostingOrder.objects.get( - subscription_id=sub_ids[0] - ) + try: + args['order'] = HostingOrder.objects.get( + subscription_id=sub_ids[0] + ) + except HostingOrder.DoesNotExist as dne: + logger.error("Hosting order for {} doesn't exist".format( + sub_ids[0] + )) + args['order'] = None else: logger.debug( "More than one subscriptions" From 3b26b94fd375f8acf091c3f335d2be30f454b7ab Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 15 Apr 2019 22:27:11 +0200 Subject: [PATCH 12/30] Add logger message --- webhook/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webhook/views.py b/webhook/views.py index 8b8a65b0..8a2bb712 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -100,6 +100,8 @@ def handle_invoice_webhook(request): '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": From 3c215638f5464c85d0127cb3f87361a0ff0f83aa Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 15 Apr 2019 22:57:02 +0200 Subject: [PATCH 13/30] Send error mails to admin only --- webhook/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook/views.py b/webhook/views.py index 8a2bb712..0542448a 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -130,7 +130,7 @@ def handle_error(error_msg, error_body): email_to_admin_data = { 'subject': error_msg, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': settings.WEBHOOK_EMAIL_TO.split(","), + 'to': [settings.ADMIN_EMAIL], 'body': error_body, } send_plain_email_task.delay(email_to_admin_data) From d64b6329abf37e4124dd80ae62bfff16ae2ed19b Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 15 Apr 2019 23:17:38 +0200 Subject: [PATCH 14/30] Fallback to obtain VM_ID from order if not in metadata --- hosting/views.py | 93 +++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index fe13ff21..da139f0b 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1234,6 +1234,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 @@ -1262,33 +1263,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']) - price, vat, vat_percent, discount = get_vm_price_with_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') - ) - context['vm']['vat'] = vat - context['vm']['price'] = price - context['vm']['discount'] = discount - context['vm']['vat_percent'] = vat_percent - context['vm']['total_price'] = price + vat - discount['amount'] - 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.email, - password=self.request.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']) price, vat, vat_percent, discount = get_vm_price_with_vat( cpu=context['vm']['cores'], ssd_size=context['vm']['disk_size'], @@ -1300,20 +1290,43 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): context['vm']['price'] = price context['vm']['discount'] = discount context['vm']['vat_percent'] = vat_percent - context['vm']['total_price'] = ( - price + vat - discount['amount'] - ) - 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 + context['vm']['total_price'] = price + vat - discount['amount'] + except VMDetail.DoesNotExist: + # fallback to get it from the infrastructure + try: + manager = OpenNebulaManager( + email=self.request.email, + password=self.request.password + ) + vm = manager.get_vm(vm_id) + context['vm'] = VirtualMachineSerializer(vm).data + price, vat, vat_percent, discount = get_vm_price_with_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') + ) + context['vm']['vat'] = vat + context['vm']['price'] = price + context['vm']['discount'] = discount + context['vm']['vat_percent'] = vat_percent + context['vm']['total_price'] = ( + price + vat - discount['amount'] + ) + 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() From 22accdd0d07ffac0ac0a16383a167198b16d6e3b Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 16 Apr 2019 00:02:44 +0200 Subject: [PATCH 15/30] Fallback to vm_id from order if its not set in metadata --- webhook/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webhook/views.py b/webhook/views.py index 0542448a..aabbf9c4 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -88,6 +88,9 @@ def handle_invoice_webhook(request): 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, From 6f08a0e7da728afc264b2fc92614c8f0f7f8a8a4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 16 Apr 2019 00:15:37 +0200 Subject: [PATCH 16/30] Redirect users to invoice instead of orders --- .../templates/hosting/virtual_machine_detail.html | 2 +- hosting/views.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html index ce02036f..52fff9d6 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/{% trans "Month" %}
- {% trans "See Invoice" %} + {% trans "See Invoice" %}
diff --git a/hosting/views.py b/hosting/views.py index da139f0b..fa2f177e 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1570,8 +1570,21 @@ class VirtualMachineView(LoginRequiredMixin, View): 'virtual_machine': serializer.data, 'order': HostingOrder.objects.get( vm_id=serializer.data['vm_id'] - ) + ), + '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, From 0d4287d36ffab8506cd0b136b232976258833a07 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 12 Jun 2019 06:07:16 +0200 Subject: [PATCH 17/30] Add missing param --- webhook/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webhook/views.py b/webhook/views.py index aabbf9c4..971d1d98 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -1,15 +1,16 @@ import logging -import stripe +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 membership.models import StripeCustomer from hosting.models import MonthlyHostingBill, HostingBillLineItem +from membership.models import StripeCustomer from utils.tasks import send_plain_email_task + logger = logging.getLogger(__name__) @@ -110,7 +111,7 @@ def handle_invoice_webhook(request): elif event.type == "invoice.payment_failed": logger.error("Invoice payment failed") - admin_msg_sub = "Invoice payment FAILED for user {} and ".format( + admin_msg_sub = "Invoice payment FAILED for user {} and {}".format( stripe_customer.user.email, invoice.lines.data.metadata ) From ecdc0c32fb2c03afa295cbd447aadee367b9a223 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 12 Jun 2019 07:29:19 +0200 Subject: [PATCH 18/30] Improve logger message --- webhook/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webhook/views.py b/webhook/views.py index 971d1d98..f768e55d 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -109,12 +109,11 @@ def handle_invoice_webhook(request): send_plain_email_task.delay(email_to_admin_data) elif event.type == "invoice.payment_failed": - logger.error("Invoice payment failed") - admin_msg_sub = "Invoice payment FAILED for user {} and {}".format( stripe_customer.user.email, invoice.lines.data.metadata ) + logger.error(admin_msg_sub) email_to_admin_data = { 'subject': admin_msg_sub, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, From 68ff2c8520871ab8570ecf903622ca76759f5056 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 12 Jun 2019 07:29:49 +0200 Subject: [PATCH 19/30] Add model for failed invoices -- FailedInvoice --- hosting/models.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index 3df69c99..e1bcdd63 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,11 +1,11 @@ import json import logging import os -import pytz +from datetime import datetime +import pytz from Crypto.PublicKey import RSA from dateutil.relativedelta import relativedelta -from datetime import datetime from django.db import models from django.utils import timezone from django.utils.functional import cached_property @@ -718,3 +718,29 @@ class UserCardDetail(AssignPermissionsMixin, models.Model): return ucd except UserCardDetail.DoesNotExist: return None + + +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" + ) + + From 78f4e1767e8283addb905b303831b177eaa62f9a Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 13 Jun 2019 05:18:47 +0200 Subject: [PATCH 20/30] Add invoice_failed email templates --- .../emails/invoice_failed.html | 44 +++++++++++++++++++ .../datacenterlight/emails/invoice_failed.txt | 11 +++++ 2 files changed, 55 insertions(+) create mode 100644 datacenterlight/templates/datacenterlight/emails/invoice_failed.html create mode 100644 datacenterlight/templates/datacenterlight/emails/invoice_failed.txt 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 From 690b80a61674697763b0c07f377be66ff30852f8 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 13 Jun 2019 05:19:34 +0200 Subject: [PATCH 21/30] Add FailedInvoice create method --- hosting/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/hosting/models.py b/hosting/models.py index e1bcdd63..aec95dfc 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -743,4 +743,18 @@ class FailedInvoice(AssignPermissionsMixin, models.Model): help_text="The datetime if/when service was interrupted" ) + class Meta: + permissions = ( + ('view_failedinvoice', 'View User Card'), + ) + + @classmethod + def create(cls, stripe_customer=None, order=None, invoice_id=None): + instance = cls.objects.create( + stripe_customer=stripe_customer, order=order, number_of_attempts=0, + invoice_id=invoice_id + ) + instance.assign_permissions(stripe_customer.user) + return instance + From ad846fabec075fccf4781e4aa8772d4d805f6308 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 13 Jun 2019 05:20:00 +0200 Subject: [PATCH 22/30] Send invoice failed notification to user also --- webhook/views.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/webhook/views.py b/webhook/views.py index f768e55d..7eed6141 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -9,6 +9,7 @@ from django.views.decorators.http import require_POST from hosting.models import MonthlyHostingBill, HostingBillLineItem from membership.models import StripeCustomer +from utils.mailer import BaseEmail from utils.tasks import send_plain_email_task logger = logging.getLogger(__name__) @@ -114,6 +115,7 @@ def handle_invoice_webhook(request): invoice.lines.data.metadata ) logger.error(admin_msg_sub) + # send email to admin email_to_admin_data = { 'subject': admin_msg_sub, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, @@ -121,6 +123,26 @@ def handle_invoice_webhook(request): '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, + } + 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) From e981bf1542ea73a132236e44db45fd4a089ebd0e Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 13 Jun 2019 09:54:19 +0200 Subject: [PATCH 23/30] Pass number_of_attempts as parameter also when creating FailedInvoice --- hosting/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index aec95dfc..c7a3dcde 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -749,9 +749,12 @@ class FailedInvoice(AssignPermissionsMixin, models.Model): ) @classmethod - def create(cls, stripe_customer=None, order=None, invoice_id=None): + 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=0, + stripe_customer=stripe_customer, + order=order, + number_of_attempts=number_of_attempts, invoice_id=invoice_id ) instance.assign_permissions(stripe_customer.user) From 655316305bda064307a46436865ace0f1fcefb55 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 13 Jun 2019 09:55:36 +0200 Subject: [PATCH 24/30] Create FailedInvoice when invoice.payment_failed webhook is fired Also set the context parameters for sending emails --- webhook/views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/webhook/views.py b/webhook/views.py index 7eed6141..d20b266c 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -7,7 +7,9 @@ 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 +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 @@ -110,9 +112,15 @@ def handle_invoice_webhook(request): 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, - invoice.lines.data.metadata + VM_ID ) logger.error(admin_msg_sub) # send email to admin @@ -128,6 +136,8 @@ def handle_invoice_webhook(request): '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( From 82a03ece747c4b230c459910e3ae38cb7599f6a4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 08:46:38 +0530 Subject: [PATCH 25/30] Update comment --- hosting/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hosting/models.py b/hosting/models.py index bb7c5cc1..50e79cd0 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -537,7 +537,7 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model): def get_vm_id(self): """ If VM_ID is set in the metadata extract and return it as integer - other return -1 + other return None :return: """ @@ -747,7 +747,7 @@ class FailedInvoice(AssignPermissionsMixin, models.Model): class Meta: permissions = ( - ('view_failedinvoice', 'View User Card'), + ('view_failedinvoice', 'View Failed Invoice'), ) @classmethod From 9b798b43762d1cfb0ed932309fe4c4a5d026eef4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 08:56:54 +0530 Subject: [PATCH 26/30] Add merge migration 0056_merge.py --- hosting/migrations/0056_merge.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 hosting/migrations/0056_merge.py 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 = [ + ] From 9b3e292598c40d81f3d5421f16915b42f3275b48 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 09:35:57 +0530 Subject: [PATCH 27/30] Add a 5 min trial period to subscriptions for test purposes iff ADD_TRIAL_PERIOD_TO_SUBSCRIPTION is set to true --- datacenterlight/tests.py | 7 ++++--- datacenterlight/views.py | 5 ++++- dynamicweb/settings/base.py | 1 + hosting/views.py | 3 ++- utils/tests.py | 9 ++++++--- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index ca1bb930..68f4e15d 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,7 +119,8 @@ 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_5MIN_TRIAL_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 \ diff --git a/datacenterlight/views.py b/datacenterlight/views.py index ae649623..4dd328ad 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,3 +1,4 @@ +import datetime import logging from django import forms @@ -785,7 +786,9 @@ 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_5MIN_TRIAL_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 diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index a1779d5f..8af6ddf6 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -724,6 +724,7 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else INVOICE_WEBHOOK_SECRET = env('INVOICE_WEBHOOK_SECRET') DEBUG = bool_env('DEBUG') +ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION') READ_VM_REALM = env('READ_VM_REALM') AUTH_NAME = env('AUTH_NAME') diff --git a/hosting/views.py b/hosting/views.py index e0f5ebd3..ce17376b 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1034,7 +1034,8 @@ 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.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_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 diff --git a/utils/tests.py b/utils/tests.py index 8abbbb1d..c67041d1 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_5MIN_TRIAL_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_5MIN_TRIAL_TO_SUBSCRIPTION else None) self.assertIsNone(result.get('response_object'), None) self.assertIsNotNone(result.get('error')) From 494d68bb47017c6336911ceb54daffcd7c4111f9 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 09:51:03 +0530 Subject: [PATCH 28/30] Use correct variable name ADD_TRIAL_PERIOD_TO_SUBSCRIPTION --- datacenterlight/tests.py | 2 +- datacenterlight/views.py | 2 +- hosting/views.py | 2 +- utils/tests.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index 68f4e15d..964a9d5f 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -120,7 +120,7 @@ class CeleryTaskTestCase(TestCase): stripe_customer.stripe_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_TO_SUBSCRIPTION else None) + 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 \ diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 4dd328ad..ffdd4831 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -787,7 +787,7 @@ class OrderConfirmationView(DetailView, FormView): stripe_api_cus_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_TO_SUBSCRIPTION else None + 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 diff --git a/hosting/views.py b/hosting/views.py index ce17376b..e6763f32 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1035,7 +1035,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): stripe_api_cus_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_TO_SUBSCRIPTION else None) + 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 diff --git a/utils/tests.py b/utils/tests.py index c67041d1..fb499a94 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -233,7 +233,7 @@ class StripePlanTestCase(TestStripeCustomerDescription): stripe_customer.stripe_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_TO_SUBSCRIPTION else None) + 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')) @@ -250,7 +250,7 @@ class StripePlanTestCase(TestStripeCustomerDescription): stripe_customer.stripe_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_5MIN_TRIAL_TO_SUBSCRIPTION else None) + 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')) From ee90ee562489899ef852c6d0861b6fd0523f9c75 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 09:57:45 +0530 Subject: [PATCH 29/30] Fix bug: correct the way to get timestamp from datetime --- hosting/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index e6763f32..ff45b0db 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1035,7 +1035,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): stripe_api_cus_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}], - int(datetime.datetime.now().timestamp()) + 300 if settings.ADD_TRIAL_PERIOD_TO_SUBSCRIPTION else None) + 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 From ceff7964e915d7c9e6264e7364b5bf3d8696d16a Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 18 Jul 2019 10:13:03 +0530 Subject: [PATCH 30/30] Accept subscription status = trialing because that is what we will be if we are using the trial period --- datacenterlight/tests.py | 6 ++++-- datacenterlight/views.py | 6 ++++-- hosting/views.py | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index 964a9d5f..74798cc3 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -123,8 +123,10 @@ class CeleryTaskTestCase(TestCase): 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 ffdd4831..b2fd483c 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -791,8 +791,10 @@ class OrderConfirmationView(DetailView, FormView): ) 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/hosting/views.py b/hosting/views.py index ff45b0db..729c402f 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1039,7 +1039,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView): 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