From 39bc12e3519ddacc8987761d45fb391bafa1a42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Tue, 9 Jan 2018 16:45:38 +0200 Subject: [PATCH] Invoices to email. --- hc/lib/emails.py | 12 ++++ hc/payments/forms.py | 5 -- hc/payments/invoices.py | 2 - .../0004_subscription_send_invoices.py | 20 ++++++ hc/payments/models.py | 9 +++ hc/payments/tests/test_billing.py | 16 +++++ hc/payments/tests/test_pdf_invoice.py | 9 +-- hc/payments/urls.py | 1 + hc/payments/views.py | 66 +++++++++++++++---- templates/accounts/billing.html | 49 ++++++++++++-- templates/accounts/profile.html | 2 +- templates/emails/invoice-body-html.html | 15 +++++ templates/emails/invoice-body-text.html | 8 +++ templates/emails/invoice-subject.html | 2 + 14 files changed, 187 insertions(+), 29 deletions(-) delete mode 100644 hc/payments/forms.py create mode 100644 hc/payments/migrations/0004_subscription_send_invoices.py create mode 100644 hc/payments/tests/test_billing.py create mode 100644 templates/emails/invoice-body-html.html create mode 100644 templates/emails/invoice-body-text.html create mode 100644 templates/emails/invoice-subject.html diff --git a/hc/lib/emails.py b/hc/lib/emails.py index d33cfac3..73bcbd83 100644 --- a/hc/lib/emails.py +++ b/hc/lib/emails.py @@ -58,3 +58,15 @@ def verify_email(to, ctx): def report(to, ctx): send("report", to, ctx) + + +def invoice(to, ctx, filename, pdf_data): + ctx["SITE_ROOT"] = settings.SITE_ROOT + subject = render('emails/invoice-subject.html', ctx).strip() + text = render('emails/invoice-body-text.html', ctx) + html = render('emails/invoice-body-html.html', ctx) + + msg = EmailMultiAlternatives(subject, text, to=(to, )) + msg.attach_alternative(html, "text/html") + msg.attach(filename, pdf_data, "application/pdf") + msg.send() diff --git a/hc/payments/forms.py b/hc/payments/forms.py deleted file mode 100644 index 7dd571e7..00000000 --- a/hc/payments/forms.py +++ /dev/null @@ -1,5 +0,0 @@ -from django import forms - - -class BillToForm(forms.Form): - bill_to = forms.CharField(max_length=500, required=False) diff --git a/hc/payments/invoices.py b/hc/payments/invoices.py index f85a3a75..5391cf8a 100644 --- a/hc/payments/invoices.py +++ b/hc/payments/invoices.py @@ -93,8 +93,6 @@ class PdfInvoice(Canvas): self.text(s.strip()) self.linefeed() - self.text("If you have a credit card on file it will be " - "automatically charged within 24 hours.", align="center") self.showPage() self.save() diff --git a/hc/payments/migrations/0004_subscription_send_invoices.py b/hc/payments/migrations/0004_subscription_send_invoices.py new file mode 100644 index 00000000..568a7894 --- /dev/null +++ b/hc/payments/migrations/0004_subscription_send_invoices.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-01-09 12:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0003_subscription_address_id'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='send_invoices', + field=models.BooleanField(default=True), + ), + ] diff --git a/hc/payments/models.py b/hc/payments/models.py index 35e8104c..eb953351 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.db import models +from django.template.loader import render_to_string if settings.USE_PAYMENTS: import braintree @@ -29,6 +30,7 @@ class Subscription(models.Model): subscription_id = models.CharField(max_length=10, blank=True) plan_id = models.CharField(max_length=10, blank=True) address_id = models.CharField(max_length=2, blank=True) + send_invoices = models.BooleanField(default=True) objects = SubscriptionManager() @@ -176,6 +178,13 @@ class Subscription(models.Model): return self._address + def flattened_address(self): + if self.address_id: + ctx = {"a": self.address} + return render_to_string("payments/address_plain.html", ctx) + else: + return self.user.email + @property def transactions(self): if not hasattr(self, "_tx"): diff --git a/hc/payments/tests/test_billing.py b/hc/payments/tests/test_billing.py new file mode 100644 index 00000000..bba7f2bf --- /dev/null +++ b/hc/payments/tests/test_billing.py @@ -0,0 +1,16 @@ +from mock import patch + +from hc.payments.models import Subscription +from hc.test import BaseTestCase + + +class SetPlanTestCase(BaseTestCase): + + @patch("hc.payments.models.braintree") + def test_it_saves_send_invoices_flag(self, mock): + self.client.login(username="alice@example.org", password="password") + + form = {"save_send_invoices": True} + self.client.post("/accounts/profile/billing/", form) + sub = Subscription.objects.get() + self.assertFalse(sub.send_invoices) diff --git a/hc/payments/tests/test_pdf_invoice.py b/hc/payments/tests/test_pdf_invoice.py index 85bc13f2..35738892 100644 --- a/hc/payments/tests/test_pdf_invoice.py +++ b/hc/payments/tests/test_pdf_invoice.py @@ -55,11 +55,12 @@ class PdfInvoiceTestCase(BaseTestCase): @skipIf(reportlab is None, "reportlab not installed") @patch("hc.payments.models.braintree") - def test_it_shows_company_data(self, mock_braintree): - self.profile.bill_to = "Alice and Partners" - self.profile.save() + def test_it_shows_company_data(self, mock): + self.sub.address_id = "aa" + self.sub.save() - mock_braintree.Transaction.find.return_value = self.tx + mock.Transaction.find.return_value = self.tx + mock.Address.find.return_value = {"company": "Alice and Partners"} self.client.login(username="alice@example.org", password="password") r = self.client.get("/invoice/pdf/abc123/") diff --git a/hc/payments/urls.py b/hc/payments/urls.py index 7256e6b2..0fdb0966 100644 --- a/hc/payments/urls.py +++ b/hc/payments/urls.py @@ -35,4 +35,5 @@ urlpatterns = [ views.get_client_token, name="hc-get-client-token"), + url(r'^pricing/charge/$', views.charge_webhook), ] diff --git a/hc/payments/views.py b/hc/payments/views.py index fba84efb..98eb7646 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -4,9 +4,11 @@ from django.http import (HttpResponseBadRequest, HttpResponseForbidden, JsonResponse, HttpResponse) from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string +from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST - +import six from hc.api.models import Check +from hc.lib import emails from hc.payments.invoices import PdfInvoice from hc.payments.models import Subscription @@ -37,15 +39,37 @@ def billing(request): # subscription object is not created just by viewing a page. sub = Subscription.objects.filter(user_id=request.user.id).first() + send_invoices_status = "default" + if request.method == "POST": + if "save_send_invoices" in request.POST: + sub = Subscription.objects.for_user(request.user) + sub.send_invoices = "send_invoices" in request.POST + sub.save() + send_invoices_status = "success" + ctx = { "page": "billing", "profile": request.profile, "sub": sub, "num_checks": Check.objects.filter(user=request.user).count(), "team_size": request.profile.member_set.count() + 1, - "team_max": request.profile.team_limit + 1 + "team_max": request.profile.team_limit + 1, + "send_invoices_status": send_invoices_status, + "set_plan_status": "default", + "address_status": "default", + "payment_method_status": "default" } + if "set_plan_status" in request.session: + ctx["set_plan_status"] = request.session.pop("set_plan_status") + + if "address_status" in request.session: + ctx["address_status"] = request.session.pop("address_status") + + if "payment_method_status" in request.session: + ctx["payment_method_status"] = \ + request.session.pop("payment_method_status") + return render(request, "accounts/billing.html", ctx) @@ -105,7 +129,7 @@ def set_plan(request): profile.sms_sent = 0 profile.save() - request.session["first_charge"] = True + request.session["set_plan_status"] = "success" return redirect("hc-billing") @@ -117,6 +141,7 @@ def address(request): if error: return log_and_bail(request, error) + request.session["address_status"] = "success" return redirect("hc-billing") ctx = {"a": sub.address} @@ -136,6 +161,7 @@ def payment_method(request): if error: return log_and_bail(request, error) + request.session["payment_method_status"] = "success" return redirect("hc-billing") ctx = { @@ -167,15 +193,29 @@ def pdf_invoice(request, transaction_id): response = HttpResponse(content_type='application/pdf') filename = "MS-HC-%s.pdf" % transaction.id.upper() response['Content-Disposition'] = 'attachment; filename="%s"' % filename + PdfInvoice(response).render(transaction, sub.flattened_address()) + return response - bill_to = [] - if sub.address_id: - ctx = {"a": sub.address} - bill_to = render_to_string("payments/address_plain.html", ctx) - elif request.user.profile.bill_to: - bill_to = request.user.profile.bill_to - else: - bill_to = request.user.email - PdfInvoice(response).render(transaction, bill_to) - return response +@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) + if sub.send_invoices: + transaction = doc.subscription.transactions[0] + filename = "MS-HC-%s.pdf" % transaction.id.upper() + + sink = six.BytesIO() + PdfInvoice(sink).render(transaction, sub.flattened_address()) + ctx = {"tx": transaction} + emails.invoice(sub.user.email, ctx, filename, sink.getvalue()) + + return HttpResponse() diff --git a/templates/accounts/billing.html b/templates/accounts/billing.html index 096e0c1e..cf33b984 100644 --- a/templates/accounts/billing.html +++ b/templates/accounts/billing.html @@ -35,7 +35,7 @@
-
+

Billing Plan

@@ -96,9 +96,14 @@ Change Billing Plan
+ {% if set_plan_status == "success" %} + + {% endif %}
-
+

Payment Method

{% if sub.payment_method_token %} @@ -113,10 +118,15 @@ class="btn btn-default pull-right"> Change Payment Method
+ {% if payment_method_status == "success" %} + + {% endif %}
-
+

Billing Address

@@ -137,7 +147,7 @@ Change Billing Address
- {% if status == "info" %} + {% if address_status == "success" %} @@ -146,6 +156,37 @@
+
+
+
+ {% csrf_token %} +

Invoices to Email

+ + + + +
+
+ {% if send_invoices_status == "success" %} + + {% endif %} +
+

Billing History

diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 68b48867..3de48784 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -57,7 +57,7 @@
-
+

API Access

{% if profile.api_key %} diff --git a/templates/emails/invoice-body-html.html b/templates/emails/invoice-body-html.html new file mode 100644 index 00000000..05c85a1b --- /dev/null +++ b/templates/emails/invoice-body-html.html @@ -0,0 +1,15 @@ +{% extends "emails/base.html" %} +{% load hc_extras %} + +{% block content %} +Hello,
+Here's your invoice from {% site_name %} for +{{ tx.subscription_details.billing_period_start_date }} - {{ tx.subscription_details.billing_period_end_date }} +.

+ +{% endblock %} + +{% block content_more %} +Thanks,
+The {% escaped_site_name %} Team +{% endblock %} diff --git a/templates/emails/invoice-body-text.html b/templates/emails/invoice-body-text.html new file mode 100644 index 00000000..132e3603 --- /dev/null +++ b/templates/emails/invoice-body-text.html @@ -0,0 +1,8 @@ +{% load hc_extras %} +Hello, + +Here's your invoice from {% site_name %}. + +-- +Regards, +{% site_name %} diff --git a/templates/emails/invoice-subject.html b/templates/emails/invoice-subject.html new file mode 100644 index 00000000..c4c2085b --- /dev/null +++ b/templates/emails/invoice-subject.html @@ -0,0 +1,2 @@ +{% load hc_extras %} +Invoice from {% site_name %}