From cf2be43bcc2aa7fddff96ce22e78525b7c700e69 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 30 Mar 2015 17:19:57 -0400 Subject: [PATCH 01/16] Rough in charge notifications --- emails/charge_succeeded.spt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 emails/charge_succeeded.spt diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt new file mode 100644 index 0000000000..5dff51fb27 --- /dev/null +++ b/emails/charge_succeeded.spt @@ -0,0 +1,8 @@ +{{ _("Your payment(s) succeeded!", user_name, platform) }} + +[---] text/html +{{ _("We charged your credit card {{ format_currency(amount) }} today, to fund your payments to {{ top_supported }}{% if %}and {{ nsupported - 1 }} others{% endif %}. Thanks for using Gratipay!", + format_currency(amount, 'USD'), + ('{1}'|safe).format(profile_url, user_name)) }} +[---] text/plain + From 284baf137b24091bde8ff2c6305cc64638afef7b Mon Sep 17 00:00:00 2001 From: Changaco Date: Fri, 6 Mar 2015 20:02:51 +0100 Subject: [PATCH 02/16] send email notifications at the end of payday --- gratipay/billing/payday.py | 20 ++++++++++++++++++++ sql/branch.sql | 3 +++ 2 files changed, 23 insertions(+) create mode 100644 sql/branch.sql diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py index f0c8bf4d47..27ba6e0949 100644 --- a/gratipay/billing/payday.py +++ b/gratipay/billing/payday.py @@ -141,6 +141,7 @@ def run(self): self.mark_stage_done() self.end() + self.notify_participants() _end = aspen.utils.utcnow() _delta = _end - _start @@ -706,6 +707,25 @@ def end(self): """, default=NoPayday).replace(tzinfo=aspen.utils.utc) + def notify_participants(self): + recs = self.db.all(""" + SELECT e.*::exchanges AS exchange, p.*::participants AS participant + FROM exchanges e + JOIN participants p ON e.participant = p.username + WHERE "timestamp" >= %(ts_start)s + AND "timestamp" < %(ts_end)s + AND amount > 0 + AND p.notify_charge > 0 + """) + for e, p in recs: + if e.status not in ('failed', 'succeeded'): + log('exchange %s has an unexpected status: %s' % (e.id, e.status)) + continue + i = 1 if e.status == 'failed' else 2 + if p.notify_charge & i: + p.queue_email('charge_'+e.status, exchange=e) + + # Record-keeping. # =============== diff --git a/sql/branch.sql b/sql/branch.sql new file mode 100644 index 0000000000..180d959a89 --- /dev/null +++ b/sql/branch.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE participants ADD COLUMN notify_charge int DEFAULT 1; +END; From b0c6a8e31fd92bce0f0e8cbd6a37ee2c7afddf99 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 8 Apr 2015 11:22:39 +0200 Subject: [PATCH 03/16] fix and complete email content --- emails/charge_succeeded.spt | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt index 5dff51fb27..9e4e7f22a1 100644 --- a/emails/charge_succeeded.spt +++ b/emails/charge_succeeded.spt @@ -1,8 +1,21 @@ -{{ _("Your payment(s) succeeded!", user_name, platform) }} +{{ ngettext("Your payment succeeded!", "Your payments succeeded!", npayments) }} [---] text/html -{{ _("We charged your credit card {{ format_currency(amount) }} today, to fund your payments to {{ top_supported }}{% if %}and {{ nsupported - 1 }} others{% endif %}. Thanks for using Gratipay!", - format_currency(amount, 'USD'), - ('{1}'|safe).format(profile_url, user_name)) }} +{{ ngettext( + "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", + "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", + format_currency(amount, 'USD'), + ('{1}'|safe).format( + participant.profile_url+'giving/', + top_tippee.username if nsupported == 1 else ngettext('{0} and {n} other', '{0} and {n} others', nsupported - 1, top_tippee.username) + )) }} + [---] text/plain +{{ ngettext( + "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", + "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", + format_currency(amount, 'USD'), + top_tippee.username if nsupported == 1 else ngettext('{0} and {n} other', '{0} and {n} others', nsupported - 1, top_tippee.username) + ) }} +{{ _("Follow this link if you want to view or modify your payments:") }} {{ participant.profile_url+'giving/' }} From fb651a7f7844282af80642ee9fd919a0aac5e75f Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 8 Apr 2015 11:37:56 +0200 Subject: [PATCH 04/16] wire up email variables --- emails/charge_succeeded.spt | 10 ++++---- gratipay/billing/payday.py | 44 ++++++++++++++++++++++++++++----- gratipay/models/participant.py | 7 ++++++ tests/py/test_billing_payday.py | 4 +-- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt index 9e4e7f22a1..b104c1812c 100644 --- a/emails/charge_succeeded.spt +++ b/emails/charge_succeeded.spt @@ -1,21 +1,21 @@ -{{ ngettext("Your payment succeeded!", "Your payments succeeded!", npayments) }} +{{ ngettext("Your payment succeeded!", "Your payments succeeded!", ntippees) }} [---] text/html {{ ngettext( "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", - format_currency(amount, 'USD'), + format_currency(exchange.amount, 'USD'), ('{1}'|safe).format( participant.profile_url+'giving/', - top_tippee.username if nsupported == 1 else ngettext('{0} and {n} other', '{0} and {n} others', nsupported - 1, top_tippee.username) + top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username) )) }} [---] text/plain {{ ngettext( "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", - format_currency(amount, 'USD'), - top_tippee.username if nsupported == 1 else ngettext('{0} and {n} other', '{0} and {n} others', nsupported - 1, top_tippee.username) + format_currency(exchange.amount, 'USD'), + top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username) ) }} {{ _("Follow this link if you want to view or modify your payments:") }} {{ participant.profile_url+'giving/' }} diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py index 27ba6e0949..99ea9d40e6 100644 --- a/gratipay/billing/payday.py +++ b/gratipay/billing/payday.py @@ -708,22 +708,54 @@ def end(self): def notify_participants(self): - recs = self.db.all(""" - SELECT e.*::exchanges AS exchange, p.*::participants AS participant + ts_start, ts_end = self.ts_start, self.ts_end + exchanges = self.db.all(""" + SELECT amount, fee, note, status, p.*::participants AS participant FROM exchanges e JOIN participants p ON e.participant = p.username WHERE "timestamp" >= %(ts_start)s AND "timestamp" < %(ts_end)s AND amount > 0 AND p.notify_charge > 0 - """) - for e, p in recs: + """, locals()) + for e in exchanges: if e.status not in ('failed', 'succeeded'): log('exchange %s has an unexpected status: %s' % (e.id, e.status)) continue i = 1 if e.status == 'failed' else 2 - if p.notify_charge & i: - p.queue_email('charge_'+e.status, exchange=e) + p = e.participant + if p.notify_charge & i == 0: + continue + username = p.username + ntippees, top_tippee = self.db.one(""" + WITH tippees AS ( + SELECT p.*::participants + FROM ( SELECT DISTINCT ON (tippee) tippee, amount + FROM tips + WHERE mtime < %(ts_start)s + AND tipper = %(username)s + ORDER BY tippee, mtime DESC + ) + JOIN participants p ON p.username = t.tippee + WHERE t.amount > 0 + AND (p.goal IS NULL or p.goal >= 0) + AND p.is_suspicious IS NOT true + AND p.claimed_time < %(ts_start)s + ) + SELECT ( SELECT count(*) FROM tippees ) AS ntippees + , ( SELECT t.* + FROM tippees t + ORDER BY amount DESC + LIMIT 1 + ) AS top_tippee + """, locals()) + p.queue_email( + 'charge_'+e.status, + exchange=e, + participant=p, + ntippees=ntippees, + top_tippee=top_tippee, + ) # Record-keeping. diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 98d06aa711..4b8f46e66f 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -779,6 +779,13 @@ def get_cryptocoin_addresses(self): # Random Junk # =========== + @property + def profile_url(self): + scheme = gratipay.canonical_scheme + host = gratipay.canonical_host + username = self.username + return '{scheme}://{host}/{username}/'.format(**locals()) + def get_teams(self): """Return a list of teams this user is a member of. """ diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py index 89a5f06e0a..d5b0962bba 100644 --- a/tests/py/test_billing_payday.py +++ b/tests/py/test_billing_payday.py @@ -245,13 +245,11 @@ def test_end_raises_NoPayday(self): @mock.patch('gratipay.billing.payday.log') @mock.patch('gratipay.billing.payday.Payday.payin') - @mock.patch('gratipay.billing.payday.Payday.end') - def test_payday(self, end, payin, log): + def test_payday(self, payin, log): greeting = 'Greetings, program! It\'s PAYDAY!!!!' Payday.start().run() log.assert_any_call(greeting) assert payin.call_count == 1 - assert end.call_count == 1 class TestPayin(BalancedHarness): From 9e867335873554ce313264c6fa3093c3ea20c20a Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 8 Apr 2015 12:10:53 +0200 Subject: [PATCH 05/16] add email simplate for failed charges --- emails/charge_failed.spt | 30 ++++++++++++++++++++++++++++++ emails/verification.spt | 2 +- gratipay/models/participant.py | 5 +++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 emails/charge_failed.spt diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt new file mode 100644 index 0000000000..29136324b4 --- /dev/null +++ b/emails/charge_failed.spt @@ -0,0 +1,30 @@ +{{ ngettext("Your payment failed!", "Your payments failed!", ntippees) }} + +[---] text/html +{{ ngettext( + "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.", + "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.", + format_currency(exchange.amount, 'USD'), + top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username) + ) }} + +{{ _("The error message we received from our payment processor is:") }}
+
+{{ exchange.note }}
+
+ +{{ _("Fix your credit card") }} + +[---] text/plain +{{ ngettext( + "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.", + "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.", + format_currency(exchange.amount, 'USD'), + top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username) + ) }} + +{{ _("The error message we received from our payment processor is:") }} + +{{ exchange.note }} + +{{ _("Follow this link to fix your credit card:") }} {{ participant.profile_url+'routes/credit-card.html' }} diff --git a/emails/verification.spt b/emails/verification.spt index 880d38e0ab..c58eccb382 100644 --- a/emails/verification.spt +++ b/emails/verification.spt @@ -6,7 +6,7 @@ ('{0}'|safe).format(username)) }}

-{{ _("Yes, proceed!") }} +{{ _("Yes, proceed!") }} [---] text/plain {{ _("We've received a request to connect {0} to the {1} account on Gratipay. Sound familiar?", diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 4b8f46e66f..5550bcd3ef 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -624,6 +624,11 @@ def remove_email(self, address): def send_email(self, spt_name, **context): context['username'] = self.username + context['button_style'] = ( + "color: #fff; text-decoration:none; display:inline-block; " + "padding: 0 15px; background: #396; white-space: nowrap; " + "font: normal 14px/40px Arial, sans-serif; border-radius: 3px" + ) context.setdefault('include_unsubscribe', True) email = context.setdefault('email', self.email_address) langs = i18n.parse_accept_lang(self.email_lang or 'en') From a64d3263fdcc0dd7b1e3c93540ee01aec6a6259f Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 8 Apr 2015 21:01:09 +0200 Subject: [PATCH 06/16] add UI for the `notify_charge` setting --- js/gratipay/settings.js | 16 +++++++------ sql/branch.sql | 4 ++++ www/%username/emails/notifications.json.spt | 8 +++---- www/%username/settings/index.html.spt | 25 ++++++++++++++++----- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/js/gratipay/settings.js b/js/gratipay/settings.js index d293a539e9..d5cefc5548 100644 --- a/js/gratipay/settings.js +++ b/js/gratipay/settings.js @@ -112,22 +112,24 @@ Gratipay.settings.init = function() { }); // Wire up notification preferences - // ============================== + // ================================ $('.email-notifications input').click(function(e) { var field = $(e.target).data('field'); + var bits = $(e.target).data('bits') || 1; jQuery.ajax( { url: '../emails/notifications.json' , type: 'POST' - , data: {toggle: field} + , data: {toggle: field, bits: bits} , dataType: 'json' , success: function(data) { - if (data.msg) { - Gratipay.notification(data.msg, 'success'); - } - $(e.target).attr('checked', data[field]); + Gratipay.notification(data.msg, 'success'); + $(e.target).attr('checked', data.new_value & bits) } - , error: Gratipay.error + , error: [ + Gratipay.error, + function(){ $(e.target).attr('checked', !$(e.target).attr('checked')) }, + ] }); }); diff --git a/sql/branch.sql b/sql/branch.sql index 180d959a89..7bc2597432 100644 --- a/sql/branch.sql +++ b/sql/branch.sql @@ -1,3 +1,7 @@ BEGIN; ALTER TABLE participants ADD COLUMN notify_charge int DEFAULT 1; + ALTER TABLE participants + ALTER COLUMN notify_on_opt_in DROP DEFAULT, + ALTER COLUMN notify_on_opt_in TYPE int USING notify_on_opt_in::int, + ALTER COLUMN notify_on_opt_in SET DEFAULT 1; END; diff --git a/www/%username/emails/notifications.json.spt b/www/%username/emails/notifications.json.spt index eff46e23f2..6a35c56dab 100644 --- a/www/%username/emails/notifications.json.spt +++ b/www/%username/emails/notifications.json.spt @@ -6,15 +6,15 @@ request.allow("POST") participant = get_participant(state, restrict=True) field = request.body.get("toggle") -if field not in ["notify_on_opt_in"]: +if field not in ["notify_charge", "notify_on_opt_in"]: raise Response(400, "Invalid notification preference.") new_value = website.db.one(""" UPDATE participants - SET {0}=not {0} - WHERE username=%s + SET {0} = {0} # %s + WHERE id = %s RETURNING {0} -""".format(field), (participant.username,)) +""".format(field), (request.body.get("bits", 1), participant.id)) assert new_value is not None [---] application/json via json_dump diff --git a/www/%username/settings/index.html.spt b/www/%username/settings/index.html.spt index 8a079610db..9b13f5ba43 100644 --- a/www/%username/settings/index.html.spt +++ b/www/%username/settings/index.html.spt @@ -202,15 +202,30 @@ emails = participant.get_emails()

{{ _("Notifications") }}

-

{{ _("Close") }}

From d73d42251448e1df0c3248bc1c02b5e495688aa8 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 8 Apr 2015 21:07:32 +0200 Subject: [PATCH 07/16] fix test --- tests/py/test_email_notifs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/py/test_email_notifs.py b/tests/py/test_email_notifs.py index 78b1f5f7d8..a49c681922 100644 --- a/tests/py/test_email_notifs.py +++ b/tests/py/test_email_notifs.py @@ -49,7 +49,7 @@ def test_take_over_sends_notifications_to_patrons(self): def test_opt_in_notification_includes_unsubscribe(self): carl_twitter = self.make_elsewhere('twitter', 1, 'carl') - roy = self.make_participant('roy', claimed_time='now', email_address='roy@example.com', notify_on_opt_in=True) + roy = self.make_participant('roy', claimed_time='now', email_address='roy@example.com', notify_on_opt_in=1) roy.set_tip_to(carl_twitter.participant.username, '100') AccountElsewhere.from_user_name('twitter', 'carl').opt_in('carl') From c2cee2d33eba6b0c078bc7751ce97c0568fa9ce0 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 8 Apr 2015 18:53:31 -0400 Subject: [PATCH 08/16] Here's a failing test for notify_participants --- tests/py/test_billing_payday.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py index d5b0962bba..0414cc9941 100644 --- a/tests/py/test_billing_payday.py +++ b/tests/py/test_billing_payday.py @@ -527,3 +527,18 @@ def test_payout_ach_error(self, ach_credit): Payday.start().payout() payday = self.fetch_payday() assert payday['nach_failing'] == 1 + + +class TestNotifyParticipants(BalancedHarness): + + @mock.patch.object(Payday, 'fetch_card_holds') + def test_it_notifies_participants(self, fch): + self.janet.set_tip_to(self.homer, D('10.00')) + fch.return_value = {} + Payday.start().run() + + exchanges = self.db.one('SELECT * FROM exchanges') + assert exchanges['amount'] == D('10.00') + + emails = self.db.one('SELECT * FROM email_queue') + assert emails['spt_name'] == 'charge_succeeded' From bd5f789323b6ab44a162b3df27a4d4d1fa263a23 Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 9 Apr 2015 10:46:19 +0200 Subject: [PATCH 09/16] modify test to avoid test fixtures and cover both email simplates --- tests/py/test_billing_payday.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py index 0414cc9941..5bb4a82730 100644 --- a/tests/py/test_billing_payday.py +++ b/tests/py/test_billing_payday.py @@ -12,6 +12,7 @@ from gratipay.models.participant import Participant from gratipay.testing import Foobar, Harness from gratipay.testing.balanced import BalancedHarness +from gratipay.testing.emails import EmailHarness class TestPayday(BalancedHarness): @@ -529,16 +530,22 @@ def test_payout_ach_error(self, ach_credit): assert payday['nach_failing'] == 1 -class TestNotifyParticipants(BalancedHarness): +class TestNotifyParticipants(EmailHarness): - @mock.patch.object(Payday, 'fetch_card_holds') - def test_it_notifies_participants(self, fch): - self.janet.set_tip_to(self.homer, D('10.00')) - fch.return_value = {} - Payday.start().run() + def test_it_notifies_participants(self): + kalel = self.make_participant('kalel', claimed_time='now', is_suspicious=False, + email_address='kalel@example.net', notify_charge=3) + lily = self.make_participant('lily', claimed_time='now', is_suspicious=False) + kalel.set_tip_to(lily, 10) + + for status in ('failed', 'succeeded'): + payday = Payday.start() + self.make_exchange('balanced-cc', 10, 0, kalel, status) + payday.end() + payday.notify_participants() - exchanges = self.db.one('SELECT * FROM exchanges') - assert exchanges['amount'] == D('10.00') + emails = self.db.one('SELECT * FROM email_queue') + assert emails.spt_name == 'charge_'+status - emails = self.db.one('SELECT * FROM email_queue') - assert emails['spt_name'] == 'charge_succeeded' + Participant.dequeue_emails() + assert self.get_last_email()['to'][0]['email'] == 'kalel@example.net' From 047b0c2893e359a253704874abc13a26d13b82bb Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 9 Apr 2015 11:08:19 +0200 Subject: [PATCH 10/16] fix payday notifications --- emails/charge_failed.spt | 6 ++++-- emails/charge_succeeded.spt | 6 ++++-- gratipay/billing/payday.py | 11 +++++------ gratipay/models/participant.py | 1 + 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt index 29136324b4..519f384444 100644 --- a/emails/charge_failed.spt +++ b/emails/charge_failed.spt @@ -4,8 +4,9 @@ {{ ngettext( "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.", "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.", + ntippees, format_currency(exchange.amount, 'USD'), - top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username) + top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee) ) }} {{ _("The error message we received from our payment processor is:") }}
@@ -19,8 +20,9 @@
 {{ ngettext(
     "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.",
     "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.",
+    ntippees,
     format_currency(exchange.amount, 'USD'),
-    top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username)
+    top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     ) }}
 
 {{ _("The error message we received from our payment processor is:") }}
diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt
index b104c1812c..d05e26afbe 100644
--- a/emails/charge_succeeded.spt
+++ b/emails/charge_succeeded.spt
@@ -4,18 +4,20 @@
 {{ ngettext(
     "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!",
     "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!",
+    ntippees,
     format_currency(exchange.amount, 'USD'),
     ('{1}'|safe).format(
         participant.profile_url+'giving/',
-        top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username)
+        top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     )) }}
 
 [---] text/plain
 {{ ngettext(
     "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!",
     "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!",
+    ntippees,
     format_currency(exchange.amount, 'USD'),
-    top_tippee.username if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee.username)
+    top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     ) }}
 
 {{ _("Follow this link if you want to view or modify your payments:") }} {{ participant.profile_url+'giving/' }}
diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py
index 99ea9d40e6..94dade64cf 100644
--- a/gratipay/billing/payday.py
+++ b/gratipay/billing/payday.py
@@ -729,13 +729,13 @@ def notify_participants(self):
             username = p.username
             ntippees, top_tippee = self.db.one("""
                 WITH tippees AS (
-                         SELECT p.*::participants
+                         SELECT p.username, amount
                            FROM ( SELECT DISTINCT ON (tippee) tippee, amount
                                     FROM tips
                                    WHERE mtime < %(ts_start)s
                                      AND tipper = %(username)s
                                 ORDER BY tippee, mtime DESC
-                                )
+                                ) t
                            JOIN participants p ON p.username = t.tippee
                           WHERE t.amount > 0
                             AND (p.goal IS NULL or p.goal >= 0)
@@ -743,16 +743,15 @@ def notify_participants(self):
                             AND p.claimed_time < %(ts_start)s
                      )
                 SELECT ( SELECT count(*) FROM tippees ) AS ntippees
-                     , ( SELECT t.*
-                           FROM tippees t
+                     , ( SELECT username
+                           FROM tippees
                        ORDER BY amount DESC
                           LIMIT 1
                        ) AS top_tippee
             """, locals())
             p.queue_email(
                 'charge_'+e.status,
-                exchange=e,
-                participant=p,
+                exchange=dict(amount=e.amount, fee=e.fee, note=e.note),
                 ntippees=ntippees,
                 top_tippee=top_tippee,
             )
diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py
index 5550bcd3ef..0b658e0a3f 100644
--- a/gratipay/models/participant.py
+++ b/gratipay/models/participant.py
@@ -623,6 +623,7 @@ def remove_email(self, address):
                   (self.username, address))
 
     def send_email(self, spt_name, **context):
+        context['participant'] = self
         context['username'] = self.username
         context['button_style'] = (
             "color: #fff; text-decoration:none; display:inline-block; "

From 5973342ab05b198a55b49eda1f180e2c7512eb7e Mon Sep 17 00:00:00 2001
From: Changaco 
Date: Thu, 9 Apr 2015 23:41:28 +0200
Subject: [PATCH 11/16] send notifications for successful charges by default

---
 sql/branch.sql | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sql/branch.sql b/sql/branch.sql
index 7bc2597432..577a220d79 100644
--- a/sql/branch.sql
+++ b/sql/branch.sql
@@ -1,5 +1,5 @@
 BEGIN;
-    ALTER TABLE participants ADD COLUMN notify_charge int DEFAULT 1;
+    ALTER TABLE participants ADD COLUMN notify_charge int DEFAULT 3;
     ALTER TABLE participants
         ALTER COLUMN notify_on_opt_in DROP DEFAULT,
         ALTER COLUMN notify_on_opt_in TYPE int USING notify_on_opt_in::int,

From a5c9f8f22f5da419cb671b2b28a775af00f53757 Mon Sep 17 00:00:00 2001
From: Chad Whitacre 
Date: Mon, 13 Apr 2015 17:56:02 -0400
Subject: [PATCH 12/16] Show total amount charged, including processor fee

---
 emails/charge_failed.spt    | 4 ++--
 emails/charge_succeeded.spt | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt
index 519f384444..a542640077 100644
--- a/emails/charge_failed.spt
+++ b/emails/charge_failed.spt
@@ -5,7 +5,7 @@
     "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.",
     "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.",
     ntippees,
-    format_currency(exchange.amount, 'USD'),
+    format_currency(exchange.amount + exchange.fee, 'USD'),
     top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     ) }}
 
@@ -21,7 +21,7 @@
     "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.",
     "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.",
     ntippees,
-    format_currency(exchange.amount, 'USD'),
+    format_currency(exchange.amount + exchange.fee, 'USD'),
     top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     ) }}
 
diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt
index d05e26afbe..4cd59942ba 100644
--- a/emails/charge_succeeded.spt
+++ b/emails/charge_succeeded.spt
@@ -5,7 +5,7 @@
     "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!",
     "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!",
     ntippees,
-    format_currency(exchange.amount, 'USD'),
+    format_currency(exchange.amount + exchange.fee, 'USD'),
     ('{1}'|safe).format(
         participant.profile_url+'giving/',
         top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
@@ -16,7 +16,7 @@
     "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!",
     "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!",
     ntippees,
-    format_currency(exchange.amount, 'USD'),
+    format_currency(exchange.amount + exchange.fee, 'USD'),
     top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
     ) }}
 

From 3e39acd50a4b00dbcf0452cb3e8cdddd582f8546 Mon Sep 17 00:00:00 2001
From: Chad Whitacre 
Date: Mon, 13 Apr 2015 18:43:38 -0400
Subject: [PATCH 13/16] Tweak language of charge notifications

---
 emails/charge_failed.spt    | 41 +++++++++++++++++--------------------
 emails/charge_succeeded.spt | 30 +++++++++++++--------------
 2 files changed, 33 insertions(+), 38 deletions(-)

diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt
index a542640077..6686b4cfcd 100644
--- a/emails/charge_failed.spt
+++ b/emails/charge_failed.spt
@@ -1,31 +1,28 @@
-{{ ngettext("Your payment failed!", "Your payments failed!", ntippees) }}
+{{ _("Oh no! A problem supporting {0}!", top_tippee) }}
 
 [---] text/html
-{{ ngettext(
-    "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.",
-    "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.",
-    ntippees,
-    format_currency(exchange.amount + exchange.fee, 'USD'),
-    top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee)
-    ) }}
+{{ _("We tried to charge your credit card {0} today, to fund your ongoing support for {1}, but the charge failed with this message:",
+     format_currency(exchange.amount + exchange.fee, 'USD'),
+     ('{1}'|safe).format(
+        participant.profile_url+'giving/',
+        top_tippee if ntippees == 1 else ngettext('{0} and {n} other', 
+                                                  '{0} and {n} others', 
+                                                  ntippees - 1, 
+                                                  top_tippee))) }}
 
-{{ _("The error message we received from our payment processor is:") }}
+
{{ exchange.note }}
-{{ exchange.note }} -
- -{{ _("Fix your credit card") }} +{{ _("Fix your credit card?") }} [---] text/plain -{{ ngettext( - "We tried to charge your credit card {0} today, to fund your weekly payment to {1}, but it failed.", - "We tried to charge your credit card {0} today, to fund your weekly payments to {1}, but it failed.", - ntippees, - format_currency(exchange.amount + exchange.fee, 'USD'), - top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee) - ) }} - -{{ _("The error message we received from our payment processor is:") }} +{{ _("We tried to charge your credit card {0} today, to fund your ongoing support for {1}, but the charge failed with this message:", + ntippees, + format_currency(exchange.amount + exchange.fee, 'USD'), + top_tippee if ntippees == 1 else ngettext('{0} and {n} other', + '{0} and {n} others', + ntippees - 1, + top_tippee)) }} {{ exchange.note }} diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt index 4cd59942ba..0ace696d6b 100644 --- a/emails/charge_succeeded.spt +++ b/emails/charge_succeeded.spt @@ -1,23 +1,21 @@ -{{ ngettext("Your payment succeeded!", "Your payments succeeded!", ntippees) }} +{{ _("Thanks for supporting {0}!", top_tippee) }} [---] text/html -{{ ngettext( - "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", - "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", - ntippees, - format_currency(exchange.amount + exchange.fee, 'USD'), - ('{1}'|safe).format( +{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Gratipay!", + format_currency(exchange.amount + exchange.fee, 'USD'), + ('{1}'|safe).format( participant.profile_url+'giving/', - top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee) - )) }} + top_tippee if ntippees == 1 else ngettext('{0} and {n} other', + '{0} and {n} others', + ntippees - 1, + top_tippee))) }} [---] text/plain -{{ ngettext( - "We charged your credit card {0} today, to fund your weekly payment to {1}. Thanks for using Gratipay!", - "We charged your credit card {0} today, to fund your weekly payments to {1}. Thanks for using Gratipay!", - ntippees, - format_currency(exchange.amount + exchange.fee, 'USD'), - top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', ntippees - 1, top_tippee) - ) }} +{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Gratipay!", + format_currency(exchange.amount + exchange.fee, 'USD'), + top_tippee if ntippees == 1 else ngettext('{0} and {n} other', + '{0} and {n} others', + ntippees - 1, + top_tippee)) }} {{ _("Follow this link if you want to view or modify your payments:") }} {{ participant.profile_url+'giving/' }} From f37d238ce5bebd64426e484b54822ca9da709e93 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 13 Apr 2015 19:48:01 -0400 Subject: [PATCH 14/16] Link to receipt for successful charges --- emails/charge_failed.spt | 2 +- emails/charge_succeeded.spt | 6 ++++++ gratipay/billing/payday.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt index 6686b4cfcd..b230499242 100644 --- a/emails/charge_failed.spt +++ b/emails/charge_failed.spt @@ -13,7 +13,7 @@
{{ exchange.note }}
{{ _("Fix your credit card?") }} + style="{{ button_style }}">{{ _("Fix Credit Card") }} [---] text/plain {{ _("We tried to charge your credit card {0} today, to fund your ongoing support for {1}, but the charge failed with this message:", diff --git a/emails/charge_succeeded.spt b/emails/charge_succeeded.spt index 0ace696d6b..601b4df4cc 100644 --- a/emails/charge_succeeded.spt +++ b/emails/charge_succeeded.spt @@ -9,6 +9,10 @@ '{0} and {n} others', ntippees - 1, top_tippee))) }} +
+
+{{ _("View Receipt") }} [---] text/plain {{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Gratipay!", @@ -18,4 +22,6 @@ ntippees - 1, top_tippee)) }} +{{ _("Follow this link to view your receipt:") }} {{ '{}receipts/{}.html'.format(participant.profile_url, exchange.id) }} + {{ _("Follow this link if you want to view or modify your payments:") }} {{ participant.profile_url+'giving/' }} diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py index 94dade64cf..07c8babe55 100644 --- a/gratipay/billing/payday.py +++ b/gratipay/billing/payday.py @@ -710,7 +710,7 @@ def end(self): def notify_participants(self): ts_start, ts_end = self.ts_start, self.ts_end exchanges = self.db.all(""" - SELECT amount, fee, note, status, p.*::participants AS participant + SELECT e.id, amount, fee, note, status, p.*::participants AS participant FROM exchanges e JOIN participants p ON e.participant = p.username WHERE "timestamp" >= %(ts_start)s @@ -751,7 +751,7 @@ def notify_participants(self): """, locals()) p.queue_email( 'charge_'+e.status, - exchange=dict(amount=e.amount, fee=e.fee, note=e.note), + exchange=dict(id=e.id, amount=e.amount, fee=e.fee, note=e.note), ntippees=ntippees, top_tippee=top_tippee, ) From cd7f418e4ab92ed35923b22436345fbb8e355cd8 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 15 Apr 2015 10:25:54 +0200 Subject: [PATCH 15/16] fix text page of `charge_failed.spt` --- emails/charge_failed.spt | 1 - 1 file changed, 1 deletion(-) diff --git a/emails/charge_failed.spt b/emails/charge_failed.spt index b230499242..192ce2c51c 100644 --- a/emails/charge_failed.spt +++ b/emails/charge_failed.spt @@ -17,7 +17,6 @@ [---] text/plain {{ _("We tried to charge your credit card {0} today, to fund your ongoing support for {1}, but the charge failed with this message:", - ntippees, format_currency(exchange.amount + exchange.fee, 'USD'), top_tippee if ntippees == 1 else ngettext('{0} and {n} other', '{0} and {n} others', From c67335addec6a9742fdd9168b0e56c124fc26287 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 15 Apr 2015 10:34:34 +0200 Subject: [PATCH 16/16] fix `record_exchange`: store the `error` in `note` --- gratipay/billing/exchanges.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gratipay/billing/exchanges.py b/gratipay/billing/exchanges.py index 482fb24450..35775631e0 100644 --- a/gratipay/billing/exchanges.py +++ b/gratipay/billing/exchanges.py @@ -266,7 +266,7 @@ def _prep_hit(unrounded): return cents, amount_str, upcharged, fee -def record_exchange(db, route, amount, fee, participant, status, error=''): +def record_exchange(db, route, amount, fee, participant, status, error=None): """Given a Bunch of Stuff, return an int (exchange_id). Records in the exchanges table have these characteristics: @@ -284,10 +284,10 @@ def record_exchange(db, route, amount, fee, participant, status, error=''): exchange_id = cursor.one(""" INSERT INTO exchanges - (amount, fee, participant, status, route) - VALUES (%s, %s, %s, %s, %s) + (amount, fee, participant, status, route, note) + VALUES (%s, %s, %s, %s, %s, %s) RETURNING id - """, (amount, fee, participant.username, status, route.id)) + """, (amount, fee, participant.username, status, route.id, error)) if status == 'failed': propagate_exchange(cursor, participant, route, error, 0)