Compare commits
35 commits
master
...
feature/65
Author | SHA1 | Date | |
---|---|---|---|
|
b9f9c7b00e | ||
|
6873d901c9 | ||
|
ceff7964e9 | ||
|
ee90ee5624 | ||
|
494d68bb47 | ||
|
9b3e292598 | ||
|
9b798b4376 | ||
|
82a03ece74 | ||
|
8300babead | ||
|
655316305b | ||
|
e981bf1542 | ||
|
ad846fabec | ||
|
690b80a616 | ||
|
78f4e1767e | ||
|
68ff2c8520 | ||
|
ecdc0c32fb | ||
|
4147b8f891 | ||
|
0d4287d36f | ||
|
a3cca03edb | ||
|
6f08a0e7da | ||
|
22accdd0d0 | ||
|
d64b6329ab | ||
|
3c215638f5 | ||
|
3b26b94fd3 | ||
|
eb360c7406 | ||
|
c081f9e73a | ||
|
e7196af1f9 | ||
|
5cb1c136cf | ||
|
dc4ad93de8 | ||
|
3b84d6f646 | ||
|
d71bf87470 | ||
|
0ed3c84461 | ||
|
8a59c2da1e | ||
|
9b32290964 | ||
|
68538ac981 |
17 changed files with 527 additions and 74 deletions
|
@ -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>
|
|
@ -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" %}
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')),
|
||||
|
|
21
hosting/migrations/0053_auto_20190415_1952.py
Normal file
21
hosting/migrations/0053_auto_20190415_1952.py
Normal 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'),
|
||||
),
|
||||
]
|
16
hosting/migrations/0056_merge.py
Normal file
16
hosting/migrations/0056_merge.py
Normal 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 = [
|
||||
]
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
145
hosting/views.py
145
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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
0
webhook/__init__.py
Normal file
81
webhook/management/commands/webhook.py
Normal file
81
webhook/management/commands/webhook.py
Normal 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
3
webhook/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
172
webhook/views.py
Normal file
172
webhook/views.py
Normal 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)
|
Loading…
Reference in a new issue