Users can specify a separate email address that will receive invoices.

This commit is contained in:
Pēteris Caune 2018-04-24 16:06:03 +03:00
parent 157bd74aeb
commit 9fb7ca7103
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
9 changed files with 245 additions and 42 deletions

16
hc/payments/forms.py Normal file
View 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()

View 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),
),
]

View File

@ -35,6 +35,16 @@ class SubscriptionManager(models.Manager):
return sub, tx 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): class Subscription(models.Model):
user = models.OneToOneField(User, models.CASCADE, blank=True, null=True) 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) plan_name = models.CharField(max_length=50, blank=True)
address_id = models.CharField(max_length=2, blank=True) address_id = models.CharField(max_length=2, blank=True)
send_invoices = models.BooleanField(default=True) send_invoices = models.BooleanField(default=True)
invoice_email = models.EmailField(blank=True)
objects = SubscriptionManager() objects = SubscriptionManager()

View File

@ -1,16 +1,33 @@
from mock import patch
from hc.payments.models import Subscription from hc.payments.models import Subscription
from hc.test import BaseTestCase from hc.test import BaseTestCase
class SetPlanTestCase(BaseTestCase): class BillingCase(BaseTestCase):
@patch("hc.payments.models.braintree") def test_it_disables_invoice_emailing(self):
def test_it_saves_send_invoices_flag(self, mock):
self.client.login(username="alice@example.org", password="password") 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) self.client.post("/accounts/profile/billing/", form)
sub = Subscription.objects.get() sub = Subscription.objects.get()
self.assertFalse(sub.send_invoices) 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")

View 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"])

View File

@ -8,6 +8,7 @@ from django.views.decorators.http import require_POST
import six import six
from hc.api.models import Check from hc.api.models import Check
from hc.lib import emails from hc.lib import emails
from hc.payments.forms import InvoiceEmailingForm
from hc.payments.invoices import PdfInvoice from hc.payments.invoices import PdfInvoice
from hc.payments.models import Subscription from hc.payments.models import Subscription
@ -41,13 +42,15 @@ def billing(request):
# Don't use Subscription.objects.for_user method here, so a # Don't use Subscription.objects.for_user method here, so a
# subscription object is not created just by viewing a page. # subscription object is not created just by viewing a page.
sub = Subscription.objects.filter(user_id=request.user.id).first() sub = Subscription.objects.filter(user_id=request.user.id).first()
if sub is None:
sub = Subscription(user=request.user)
send_invoices_status = "default" send_invoices_status = "default"
if request.method == "POST": 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 = Subscription.objects.for_user(request.user)
sub.send_invoices = "send_invoices" in request.POST form.update_subscription(sub)
sub.save()
send_invoices_status = "success" send_invoices_status = "success"
ctx = { ctx = {
@ -208,22 +211,15 @@ def pdf_invoice(request, transaction_id):
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def charge_webhook(request): def charge_webhook(request):
sig = str(request.POST["bt_signature"]) sub, tx = Subscription.objects.by_braintree_webhook(request)
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)
if sub.send_invoices: if sub.send_invoices:
transaction = doc.subscription.transactions[0] filename = "MS-HC-%s.pdf" % tx.id.upper()
filename = "MS-HC-%s.pdf" % transaction.id.upper()
sink = six.BytesIO() sink = six.BytesIO()
PdfInvoice(sink).render(transaction, sub.flattened_address()) PdfInvoice(sink).render(tx, sub.flattened_address())
ctx = {"tx": transaction} ctx = {"tx": tx}
emails.invoice(sub.user.email, ctx, filename, sink.getvalue())
recipient = sub.invoice_email or sub.user.email
emails.invoice(recipient, ctx, filename, sink.getvalue())
return HttpResponse() return HttpResponse()

8
static/css/billing.css Normal file
View File

@ -0,0 +1,8 @@
#invoice-email {
margin-left: 50px;
width: 300px;
}
#invoice-emailing-status {
margin-right: 150px;
}

View File

@ -148,27 +148,24 @@
<div class="panel panel-{{ send_invoices_status }}"> <div class="panel panel-{{ send_invoices_status }}">
<div class="panel-body settings-block"> <div class="panel-body settings-block">
<form method="post">
{% csrf_token %}
<h2>Invoices to Email</h2> <h2>Invoices to Email</h2>
<label class="checkbox-container"> <p id="invoice-emailing-status">
<input {% if sub.send_invoices %}
name="send_invoices" Send the invoice to
type="checkbox" {{ sub.invoice_email|default:request.user.email }}
{% if sub.send_invoices %}checked{% endif %}>
<span class="checkmark"></span>
Send the invoice to {{ request.user.email }}
each time my payment method is successfully charged. each time my payment method is successfully charged.
</label> {% else %}
Do not email invoices to me.
{% endif %}
</p>
<button <button
type="submit" data-toggle="modal"
name="save_send_invoices" data-target="#invoice-emailing-modal"
class="btn btn-default pull-right"> class="btn btn-default pull-right">
Save Changes Change Preference
</button> </button>
</form>
</div> </div>
{% if send_invoices_status == "success" %} {% if send_invoices_status == "success" %}
<div class="panel-footer"> <div class="panel-footer">
@ -447,6 +444,68 @@
</form> </form>
</div> </div>
</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">&times;</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 %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@ -40,6 +40,7 @@
<link rel="stylesheet" href="{% static 'css/profile.css' %}" type="text/css"> <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/checkbox.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/radio.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 %} {% endcompress %}
</head> </head>
<body class="page-{{ page }}"> <body class="page-{{ page }}">