Compare commits

...

35 commits

Author SHA1 Message Date
PCoder
b9f9c7b00e Merge branch 'master' into feature/6561/invoices-webhook 2019-12-25 11:13:41 +05:30
PCoder
6873d901c9 Merge branch 'master' into feature/6561/invoices-webhook 2019-11-28 19:52:39 +05:30
PCoder
ceff7964e9 Accept subscription status = trialing because that is what we will be
if we are using the trial period
2019-07-18 10:13:03 +05:30
PCoder
ee90ee5624 Fix bug: correct the way to get timestamp from datetime 2019-07-18 09:57:45 +05:30
PCoder
494d68bb47 Use correct variable name ADD_TRIAL_PERIOD_TO_SUBSCRIPTION 2019-07-18 09:51:03 +05:30
PCoder
9b3e292598 Add a 5 min trial period to subscriptions for test purposes iff
ADD_TRIAL_PERIOD_TO_SUBSCRIPTION is set to true
2019-07-18 09:35:57 +05:30
PCoder
9b798b4376 Add merge migration 0056_merge.py 2019-07-18 08:56:54 +05:30
PCoder
82a03ece74 Update comment 2019-07-18 08:46:38 +05:30
PCoder
8300babead Merge branch 'master' into feature/6561/invoices-webhook 2019-07-18 08:26:15 +05:30
PCoder
655316305b Create FailedInvoice when invoice.payment_failed webhook is fired
Also set the context parameters for sending emails
2019-06-13 09:55:36 +02:00
PCoder
e981bf1542 Pass number_of_attempts as parameter also when creating FailedInvoice 2019-06-13 09:54:19 +02:00
PCoder
ad846fabec Send invoice failed notification to user also 2019-06-13 05:20:00 +02:00
PCoder
690b80a616 Add FailedInvoice create method 2019-06-13 05:19:34 +02:00
PCoder
78f4e1767e Add invoice_failed email templates 2019-06-13 05:18:47 +02:00
PCoder
68ff2c8520 Add model for failed invoices -- FailedInvoice 2019-06-12 07:29:49 +02:00
PCoder
ecdc0c32fb Improve logger message 2019-06-12 07:29:19 +02:00
PCoder
4147b8f891 Merge remote-tracking branch 'mainRepo/master' into feature/6561/invoices-webhook 2019-06-12 06:25:47 +02:00
PCoder
0d4287d36f Add missing param 2019-06-12 06:07:16 +02:00
PCoder
a3cca03edb Merge branch 'master' into feature/6561/invoices-webhook 2019-04-18 08:16:15 +02:00
PCoder
6f08a0e7da Redirect users to invoice instead of orders 2019-04-16 00:15:37 +02:00
PCoder
22accdd0d0 Fallback to vm_id from order if its not set in metadata 2019-04-16 00:02:44 +02:00
PCoder
d64b6329ab Fallback to obtain VM_ID from order if not in metadata 2019-04-15 23:17:38 +02:00
PCoder
3c215638f5 Send error mails to admin only 2019-04-15 22:57:02 +02:00
PCoder
3b26b94fd3 Add logger message 2019-04-15 22:27:11 +02:00
PCoder
eb360c7406 Make HostingOrder not mandatory in MonthlyHostingBill 2019-04-15 21:53:35 +02:00
PCoder
c081f9e73a Handle error better 2019-04-14 11:17:48 +02:00
PCoder
e7196af1f9 Fix a typo 2019-04-14 02:04:12 +02:00
PCoder
5cb1c136cf Fex checking webhook_id in options 2019-04-14 02:03:06 +02:00
PCoder
dc4ad93de8 Add list/delete functionality to webhook 2019-04-14 01:59:29 +02:00
PCoder
3b84d6f646 Handle presence of HTTP_STRIPE_SIGNATURE header in META 2019-04-14 01:36:03 +02:00
PCoder
d71bf87470 Add webhooks url 2019-04-14 01:09:53 +02:00
PCoder
0ed3c84461 Log unhandled case 2019-04-14 00:59:25 +02:00
PCoder
8a59c2da1e Implement handling invoice.payment_succeeded and invoice.payment_failed webhooks 2019-04-14 00:58:13 +02:00
PCoder
9b32290964 Add webhook app and create_webhook management command 2019-04-13 23:52:41 +02:00
PCoder
68538ac981 Update stripe version (supports stripe.Webhook) 2019-04-13 22:20:09 +02:00
17 changed files with 527 additions and 74 deletions

View file

@ -0,0 +1,44 @@
{% load static i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% trans "Data Center Light VM payment failed" %}</title>
<link rel="shortcut icon" href="{{ base_url }}{% static 'datacenterlight/img/favicon.ico' %}" type="image/x-icon">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:300,400">
</head>
<body style="margin: 0; padding: 20px 0;">
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>
<td style="padding-top: 15px;">
<h1 style="font-family: Lato, Arial, sans-serif; font-size: 25px; font-weight: 400; margin: 0;">{% trans "Data Center Light VM payment failed" %}</h1>
</td>
</tr>
<tr>
<td style="padding-top: 25px; font-size: 16px;">
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin-bottom: 10px; margin-top: 0;">
{% blocktrans %}Your invoice payment for the VM {{VM_ID}} failed.<br/><br/>Please ensure that your credit card is active and that you have sufficient credit.{% endblocktrans %}<br/><br/>
{% 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 %}<br/><br/>
{% blocktrans %}Please reply to this email or write to us at support@datacenterlight.ch if you have any queries.{% endblocktrans %}
</p>
</td>
</tr>
<tr>
<td style="padding-top: 40px; padding-bottom: 25px;">
<h3 style="font-family: Lato, Arial, sans-serif; margin: 0; font-weight: 400; font-size: 15px;">{% trans "Your Data Center Light Team" %}</h3>
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% load i18n %}
{% trans "Data Center Light VM payment failed" %}
{% blocktrans %}Your invoice payment for the VM {{VM_ID}} failed.<br/><br/>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" %}

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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')),

View file

@ -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'),
),
]

View file

@ -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 = [
]

View file

@ -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='')
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

View file

@ -46,7 +46,7 @@
<div class="vm-vmid">
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% if order.generic_product %}{% trans order.generic_product.product_subscription_interval %}{% else %}{% trans "Month" %}{% endif %}</div>
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
<a class="btn btn-vm-invoice" href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' order.pk %}{% endif %}">{% trans "See Invoice" %}</a>
</div>
</div>
<div class="vm-detail-item">

View file

@ -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,

View file

@ -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

View file

@ -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'))

0
webhook/__init__.py Normal file
View file

View file

@ -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)))

3
webhook/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

172
webhook/views.py Normal file
View file

@ -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)