diff --git a/hc/payments/forms.py b/hc/payments/forms.py new file mode 100644 index 00000000..74e7f19f --- /dev/null +++ b/hc/payments/forms.py @@ -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() diff --git a/hc/payments/migrations/0006_subscription_invoice_email.py b/hc/payments/migrations/0006_subscription_invoice_email.py new file mode 100644 index 00000000..81c2f2e0 --- /dev/null +++ b/hc/payments/migrations/0006_subscription_invoice_email.py @@ -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), + ), + ] diff --git a/hc/payments/models.py b/hc/payments/models.py index 356a1502..f09c7dad 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -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() diff --git a/hc/payments/tests/test_billing.py b/hc/payments/tests/test_billing.py index bba7f2bf..d62addbb 100644 --- a/hc/payments/tests/test_billing.py +++ b/hc/payments/tests/test_billing.py @@ -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") diff --git a/hc/payments/tests/test_charge_webhook.py b/hc/payments/tests/test_charge_webhook.py new file mode 100644 index 00000000..99213881 --- /dev/null +++ b/hc/payments/tests/test_charge_webhook.py @@ -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"]) diff --git a/hc/payments/views.py b/hc/payments/views.py index 041b838d..002e1299 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -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() diff --git a/static/css/billing.css b/static/css/billing.css new file mode 100644 index 00000000..fb79a306 --- /dev/null +++ b/static/css/billing.css @@ -0,0 +1,8 @@ +#invoice-email { + margin-left: 50px; + width: 300px; +} + +#invoice-emailing-status { + margin-right: 150px; +} \ No newline at end of file diff --git a/templates/accounts/billing.html b/templates/accounts/billing.html index 22440be2..40fe3904 100644 --- a/templates/accounts/billing.html +++ b/templates/accounts/billing.html @@ -148,27 +148,24 @@
+ {% if sub.send_invoices %} + Send the invoice to + {{ sub.invoice_email|default:request.user.email }} + each time my payment method is successfully charged. + {% else %} + Do not email invoices to me. + {% endif %} +
+ +