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
|
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()
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
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
|
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
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 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">×</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 %}
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user