Browse Source

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

py2
Pēteris Caune 7 years ago
parent
commit
9fb7ca7103
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
9 changed files with 247 additions and 44 deletions
  1. +16
    -0
      hc/payments/forms.py
  2. +20
    -0
      hc/payments/migrations/0006_subscription_invoice_email.py
  3. +11
    -0
      hc/payments/models.py
  4. +23
    -6
      hc/payments/tests/test_billing.py
  5. +75
    -0
      hc/payments/tests/test_charge_webhook.py
  6. +13
    -17
      hc/payments/views.py
  7. +8
    -0
      static/css/billing.css
  8. +80
    -21
      templates/accounts/billing.html
  9. +1
    -0
      templates/base.html

+ 16
- 0
hc/payments/forms.py 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()

+ 20
- 0
hc/payments/migrations/0006_subscription_invoice_email.py 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),
),
]

+ 11
- 0
hc/payments/models.py 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()


+ 23
- 6
hc/payments/tests/test_billing.py 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_saves_send_invoices_flag(self, mock):
def test_it_disables_invoice_emailing(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", 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="[email protected]", 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="[email protected]", password="password")
form = {"send_invoices": "2", "invoice_email": "[email protected]"}
self.client.post("/accounts/profile/billing/", form)
sub = Subscription.objects.get()
self.assertTrue(sub.send_invoices)
self.assertEqual(sub.invoice_email, "[email protected]")

+ 75
- 0
hc/payments/tests/test_charge_webhook.py 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, ["[email protected]"])
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 = "[email protected]"
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, ["[email protected]"])

+ 13
- 17
hc/payments/views.py 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
sub.save()
form.update_subscription(sub)
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"])
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: 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() 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() return HttpResponse()

+ 8
- 0
static/css/billing.css View File

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

+ 80
- 21
templates/accounts/billing.html 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>
<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 }}
each time my payment method is successfully charged.
</label>
<button
type="submit"
name="save_send_invoices"
class="btn btn-default pull-right">
Save Changes
</button>
</form>
<h2>Invoices to Email</h2>
<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.
{% else %}
Do not email invoices to me.
{% endif %}
</p>
<button
data-toggle="modal"
data-target="#invoice-emailing-modal"
class="btn btn-default pull-right">
Change Preference
</button>
</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="[email protected]"
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 %}


+ 1
- 0
templates/base.html 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 }}">


Loading…
Cancel
Save