forked from GithubBackups/healthchecks
Users can specify a separate email address that will receive invoices.
This commit is contained in:
parent
157bd74aeb
commit
9fb7ca7103
16
hc/payments/forms.py
Normal file
16
hc/payments/forms.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django import forms
|
||||
from hc.accounts.forms import LowercaseEmailField
|
||||
|
||||
|
||||
class InvoiceEmailingForm(forms.Form):
|
||||
send_invoices = forms.IntegerField(min_value=0, max_value=2)
|
||||
invoice_email = LowercaseEmailField(required=False)
|
||||
|
||||
def update_subscription(self, sub):
|
||||
sub.send_invoices = self.cleaned_data["send_invoices"] > 0
|
||||
if self.cleaned_data["send_invoices"] == 2:
|
||||
sub.invoice_email = self.cleaned_data["invoice_email"]
|
||||
else:
|
||||
sub.invoice_email = ""
|
||||
|
||||
sub.save()
|
20
hc/payments/migrations/0006_subscription_invoice_email.py
Normal file
20
hc/payments/migrations/0006_subscription_invoice_email.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2018-04-21 15:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('payments', '0005_subscription_plan_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='invoice_email',
|
||||
field=models.EmailField(blank=True, max_length=254),
|
||||
),
|
||||
]
|
@ -35,6 +35,16 @@ class SubscriptionManager(models.Manager):
|
||||
|
||||
return sub, tx
|
||||
|
||||
def by_braintree_webhook(self, request):
|
||||
sig = str(request.POST["bt_signature"])
|
||||
payload = str(request.POST["bt_payload"])
|
||||
|
||||
doc = braintree.WebhookNotification.parse(sig, payload)
|
||||
assert doc.kind == "subscription_charged_successfully"
|
||||
|
||||
sub = self.get(subscription_id=doc.subscription.id)
|
||||
return sub, doc.subscription.transactions[0]
|
||||
|
||||
|
||||
class Subscription(models.Model):
|
||||
user = models.OneToOneField(User, models.CASCADE, blank=True, null=True)
|
||||
@ -45,6 +55,7 @@ class Subscription(models.Model):
|
||||
plan_name = models.CharField(max_length=50, blank=True)
|
||||
address_id = models.CharField(max_length=2, blank=True)
|
||||
send_invoices = models.BooleanField(default=True)
|
||||
invoice_email = models.EmailField(blank=True)
|
||||
|
||||
objects = SubscriptionManager()
|
||||
|
||||
|
@ -1,16 +1,33 @@
|
||||
from mock import patch
|
||||
|
||||
from hc.payments.models import Subscription
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class SetPlanTestCase(BaseTestCase):
|
||||
class BillingCase(BaseTestCase):
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_saves_send_invoices_flag(self, mock):
|
||||
def test_it_disables_invoice_emailing(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"save_send_invoices": True}
|
||||
form = {"send_invoices": "0"}
|
||||
self.client.post("/accounts/profile/billing/", form)
|
||||
sub = Subscription.objects.get()
|
||||
self.assertFalse(sub.send_invoices)
|
||||
self.assertEqual(sub.invoice_email, "")
|
||||
|
||||
def test_it_enables_invoice_emailing(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"send_invoices": "1"}
|
||||
self.client.post("/accounts/profile/billing/", form)
|
||||
sub = Subscription.objects.get()
|
||||
self.assertTrue(sub.send_invoices)
|
||||
self.assertEqual(sub.invoice_email, "")
|
||||
|
||||
def test_it_saves_invoice_email(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"send_invoices": "2", "invoice_email": "invoices@example.org"}
|
||||
self.client.post("/accounts/profile/billing/", form)
|
||||
|
||||
sub = Subscription.objects.get()
|
||||
self.assertTrue(sub.send_invoices)
|
||||
self.assertEqual(sub.invoice_email, "invoices@example.org")
|
||||
|
75
hc/payments/tests/test_charge_webhook.py
Normal file
75
hc/payments/tests/test_charge_webhook.py
Normal file
@ -0,0 +1,75 @@
|
||||
from mock import Mock, patch
|
||||
from unittest import skipIf
|
||||
|
||||
from django.core import mail
|
||||
from django.utils.timezone import now
|
||||
from hc.payments.models import Subscription
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
try:
|
||||
import reportlab
|
||||
except ImportError:
|
||||
reportlab = None
|
||||
|
||||
|
||||
class ChargeWebhookTestCase(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ChargeWebhookTestCase, self).setUp()
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.subscription_id = "test-id"
|
||||
self.sub.customer_id = "test-customer-id"
|
||||
self.sub.send_invoices = True
|
||||
self.sub.save()
|
||||
|
||||
self.tx = Mock()
|
||||
self.tx.id = "abc123"
|
||||
self.tx.customer_details.id = "test-customer-id"
|
||||
self.tx.created_at = now()
|
||||
self.tx.currency_iso_code = "USD"
|
||||
self.tx.amount = 5
|
||||
self.tx.subscription_details.billing_period_start_date = now()
|
||||
self.tx.subscription_details.billing_period_end_date = now()
|
||||
|
||||
@skipIf(reportlab is None, "reportlab not installed")
|
||||
@patch("hc.payments.views.Subscription.objects.by_braintree_webhook")
|
||||
def test_it_works(self, mock_getter):
|
||||
mock_getter.return_value = self.sub, self.tx
|
||||
|
||||
r = self.client.post("/pricing/charge/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# See if email was sent
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
msg = mail.outbox[0]
|
||||
self.assertEqual(msg.subject, "Invoice from Mychecks")
|
||||
self.assertEqual(msg.to, ["alice@example.org"])
|
||||
self.assertEqual(msg.attachments[0][0], "MS-HC-ABC123.pdf")
|
||||
|
||||
@patch("hc.payments.views.Subscription.objects.by_braintree_webhook")
|
||||
def test_it_obeys_send_invoices_flag(self, mock_getter):
|
||||
mock_getter.return_value = self.sub, self.tx
|
||||
|
||||
self.sub.send_invoices = False
|
||||
self.sub.save()
|
||||
|
||||
r = self.client.post("/pricing/charge/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# It should not send the email
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
@skipIf(reportlab is None, "reportlab not installed")
|
||||
@patch("hc.payments.views.Subscription.objects.by_braintree_webhook")
|
||||
def test_it_uses_invoice_email(self, mock_getter):
|
||||
mock_getter.return_value = self.sub, self.tx
|
||||
|
||||
self.sub.invoice_email = "alices_accountant@example.org"
|
||||
self.sub.save()
|
||||
|
||||
r = self.client.post("/pricing/charge/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# See if the email was sent to Alice's accountant:
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].to, ["alices_accountant@example.org"])
|
@ -8,6 +8,7 @@ from django.views.decorators.http import require_POST
|
||||
import six
|
||||
from hc.api.models import Check
|
||||
from hc.lib import emails
|
||||
from hc.payments.forms import InvoiceEmailingForm
|
||||
from hc.payments.invoices import PdfInvoice
|
||||
from hc.payments.models import Subscription
|
||||
|
||||
@ -41,13 +42,15 @@ def billing(request):
|
||||
# Don't use Subscription.objects.for_user method here, so a
|
||||
# subscription object is not created just by viewing a page.
|
||||
sub = Subscription.objects.filter(user_id=request.user.id).first()
|
||||
if sub is None:
|
||||
sub = Subscription(user=request.user)
|
||||
|
||||
send_invoices_status = "default"
|
||||
if request.method == "POST":
|
||||
if "save_send_invoices" in request.POST:
|
||||
form = InvoiceEmailingForm(request.POST)
|
||||
if form.is_valid():
|
||||
sub = Subscription.objects.for_user(request.user)
|
||||
sub.send_invoices = "send_invoices" in request.POST
|
||||
sub.save()
|
||||
form.update_subscription(sub)
|
||||
send_invoices_status = "success"
|
||||
|
||||
ctx = {
|
||||
@ -208,22 +211,15 @@ def pdf_invoice(request, transaction_id):
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def charge_webhook(request):
|
||||
sig = str(request.POST["bt_signature"])
|
||||
payload = str(request.POST["bt_payload"])
|
||||
|
||||
import braintree
|
||||
doc = braintree.WebhookNotification.parse(sig, payload)
|
||||
if doc.kind != "subscription_charged_successfully":
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
sub = Subscription.objects.get(subscription_id=doc.subscription.id)
|
||||
sub, tx = Subscription.objects.by_braintree_webhook(request)
|
||||
if sub.send_invoices:
|
||||
transaction = doc.subscription.transactions[0]
|
||||
filename = "MS-HC-%s.pdf" % transaction.id.upper()
|
||||
filename = "MS-HC-%s.pdf" % tx.id.upper()
|
||||
|
||||
sink = six.BytesIO()
|
||||
PdfInvoice(sink).render(transaction, sub.flattened_address())
|
||||
ctx = {"tx": transaction}
|
||||
emails.invoice(sub.user.email, ctx, filename, sink.getvalue())
|
||||
PdfInvoice(sink).render(tx, sub.flattened_address())
|
||||
ctx = {"tx": tx}
|
||||
|
||||
recipient = sub.invoice_email or sub.user.email
|
||||
emails.invoice(recipient, ctx, filename, sink.getvalue())
|
||||
|
||||
return HttpResponse()
|
||||
|
8
static/css/billing.css
Normal file
8
static/css/billing.css
Normal file
@ -0,0 +1,8 @@
|
||||
#invoice-email {
|
||||
margin-left: 50px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#invoice-emailing-status {
|
||||
margin-right: 150px;
|
||||
}
|
@ -148,27 +148,24 @@
|
||||
|
||||
<div class="panel panel-{{ send_invoices_status }}">
|
||||
<div class="panel-body settings-block">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>Invoices to Email</h2>
|
||||
|
||||
<label class="checkbox-container">
|
||||
<input
|
||||
name="send_invoices"
|
||||
type="checkbox"
|
||||
{% if sub.send_invoices %}checked{% endif %}>
|
||||
<span class="checkmark"></span>
|
||||
Send the invoice to {{ request.user.email }}
|
||||
<p id="invoice-emailing-status">
|
||||
{% if sub.send_invoices %}
|
||||
Send the invoice to
|
||||
{{ sub.invoice_email|default:request.user.email }}
|
||||
each time my payment method is successfully charged.
|
||||
</label>
|
||||
{% else %}
|
||||
Do not email invoices to me.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
name="save_send_invoices"
|
||||
data-toggle="modal"
|
||||
data-target="#invoice-emailing-modal"
|
||||
class="btn btn-default pull-right">
|
||||
Save Changes
|
||||
Change Preference
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if send_invoices_status == "success" %}
|
||||
<div class="panel-footer">
|
||||
@ -447,6 +444,68 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="invoice-emailing-modal" class="modal pm-modal">
|
||||
<div class="modal-dialog">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4>Invoices to Email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="send_invoices"
|
||||
value="0"
|
||||
{% if not sub.send_invoices %} checked {% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Do not email invoices to me
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="send_invoices"
|
||||
value="1"
|
||||
{% if sub.send_invoices and not sub.invoice_email %} checked {% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Send invoices to {{ profile.user.email }}
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="send_invoices"
|
||||
value="2"
|
||||
{% if sub.send_invoices and sub.invoice_email %} checked {% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Send invoices to this email address:
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="invoice-email"
|
||||
name="invoice_email"
|
||||
placeholder="you@example.org"
|
||||
value="{{ sub.invoice_email }}"
|
||||
type="email"
|
||||
class="form-control" />
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -40,6 +40,7 @@
|
||||
<link rel="stylesheet" href="{% static 'css/profile.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/checkbox.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/radio.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body class="page-{{ page }}">
|
||||
|
Loading…
x
Reference in New Issue
Block a user