Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always send payin success notifications #2438

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion emails/payin_succeeded.spt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
) }}</p>
% else
<p>{{ _(
"The payment of {money_amount} initiated earlier today has succeeded.",
"The payment of {money_amount} initiated today has succeeded.",
money_amount=payin.amount,
) if payin.ctime.date() == notification_ts.date() else _(
"The payment of {money_amount} initiated on {date} has succeeded.",
Expand Down
4 changes: 2 additions & 2 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ def generate_value(self, currency):
Event('donate_reminder', 2, _("When it's time to renew my donations")),
Event('pledgee_joined', 16, _("When someone I pledge to joins Liberapay")),
Event('team_invite', 32, _("When someone invites me to join a team")),
Event('payin_failed', 2**11, _("When a payment I initiated fails")),
Event('payin_succeeded', 2**12, _("When a payment I initiated succeeds")),
Event('payin_failed', 2**11, _("When a payment from me to someone else fails")),
Event('payin_succeeded', 2**12, _("When a payment from me to someone else succeeds")),
Event('payin_refund_initiated', 2**13, _("When money is being refunded back to me")),
Event('upcoming_debit', 2**14, _("When an automatic donation renewal payment is upcoming")),
Event('missing_route', 2**15, _("When I no longer have any valid payment instrument")),
Expand Down
3 changes: 2 additions & 1 deletion liberapay/cron.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from contextvars import copy_context
from datetime import timedelta
import logging
import threading
Expand Down Expand Up @@ -165,7 +166,7 @@ def f():
if break_before_call():
break
self.running = True
r = self.func()
r = copy_context().run(self.func)
if break_after_call():
break
except Exception as e:
Expand Down
10 changes: 10 additions & 0 deletions liberapay/models/exchange_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,16 @@ def has_been_charged_successfully(self):
LIMIT 1
""", (self.participant.id, self.id)))

@property
def processor_display_name(self):
match self.network.split('-', 1)[0]:
case 'paypal':
return "PayPal"
case 'stripe':
return "Stripe"
case _:
raise NotImplementedError(self.network)

@cached_property
def stripe_payment_method(self):
return stripe.PaymentMethod.retrieve(self.address)
Expand Down
6 changes: 6 additions & 0 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from base64 import b64decode, b64encode
from collections import defaultdict
from contextvars import ContextVar
from datetime import date, timedelta
from decimal import Decimal
from email.utils import formataddr
Expand Down Expand Up @@ -115,6 +116,8 @@ class Participant(Model, MixinTeam):
ANON = False
EMAIL_VERIFICATION_TIMEOUT = EMAIL_VERIFICATION_TIMEOUT

notification_counts = ContextVar('notification_counts')

session = None

def __eq__(self, other):
Expand Down Expand Up @@ -1410,6 +1413,9 @@ def notify(self, event, force_email=False, email=True, web=True, idem_key=None,
VALUES (%(p_id)s, %(event)s, %(context)s, %(web)s, %(email)s, %(email_status)s, %(idem_key)s)
RETURNING id;
""", locals())
notif_counts = self.notification_counts.get(None)
if notif_counts is not None:
notif_counts[event] += 1
if not web:
return n_id
self.set_attributes(pending_notifs=self.pending_notifs + 1)
Expand Down
27 changes: 27 additions & 0 deletions liberapay/payin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from ..i18n.currencies import Money, MoneyBasket
from ..utils import group_by
from ..website import website


ProtoTransfer = namedtuple(
Expand Down Expand Up @@ -884,6 +885,32 @@ def update_payin_transfer(
return pt


def handle_payin_result(db, payin):
"""Notify the payer of the success or failure of a charge.
"""
assert payin.status in ('failed', 'succeeded')
payer = db.Participant.from_id(payin.payer)
if payin.status == 'succeeded':
payer.notify(
'payin_succeeded',
payin=payin._asdict(),
email_unverified_address=True,
idem_key=f"{payin.id}_{payin.status}",
)
elif payin.status == 'failed':
if website.state.get({}).get('user') == payer:
# We're about to show the payin's result to the payer.
return
route = db.ExchangeRoute.from_id(payer, payin.route)
payer.notify(
'payin_failed',
payin=payin._asdict(),
provider=route.processor_display_name,
email_unverified_address=True,
idem_key=f"{payin.id}_{payin.status}",
)


def abort_payin(db, payin, error='aborted by payer'):
"""Mark a payin as cancelled.

Expand Down
39 changes: 17 additions & 22 deletions liberapay/payin/cron.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import defaultdict
from datetime import date
from functools import wraps
from operator import itemgetter
from time import sleep

Expand All @@ -12,13 +13,26 @@
RecipientAccountSuspended, UserDoesntAcceptTips, NextAction,
)
from ..i18n.currencies import Money
from ..models.participant import Participant
from ..website import website
from ..utils import utcnow
from ..utils.types import Object
from .common import prepare_payin, resolve_tip
from .stripe import charge


def log_notification_counts(f):
@wraps(f)
def g(*args, **kw):
Participant.notification_counts.set(defaultdict(int))
r = f(*args, **kw)
for k, n in sorted(Participant.notification_counts.get().items()):
logger.info("Sent %i %s notifications.", n, k)
return r

return g


def reschedule_renewals():
"""This function looks for inconsistencies in scheduled payins.
"""
Expand Down Expand Up @@ -99,13 +113,13 @@ def reschedule_renewals():
sleep(0.1)


@log_notification_counts
def send_donation_reminder_notifications():
"""This function reminds donors to renew their donations.

The notifications are sent two weeks before the due date.
"""
db = website.db
counts = defaultdict(int)
rows = db.all("""
SELECT (SELECT p FROM participants p WHERE p.id = sp.payer) AS payer
, json_agg((SELECT a FROM (
Expand Down Expand Up @@ -169,26 +183,23 @@ def send_donation_reminder_notifications():
overdue=overdue,
email_unverified_address=True,
)
counts['donate_reminder'] += 1
db.run("""
UPDATE scheduled_payins
SET notifs_count = notifs_count + 1
, last_notif_ts = now()
WHERE payer = %s
AND id IN %s
""", (payer.id, tuple(sp['id'] for sp in payins)))
for k, n in sorted(counts.items()):
logger.info("Sent %i %s notifications." % (n, k))


@log_notification_counts
def send_upcoming_debit_notifications():
"""This daily cron job notifies donors who are about to be debited.

The notifications are sent at most once a month, 14 days before the first
payment of the "month" (31 days, not the calendar month).
"""
db = website.db
counts = defaultdict(int)
rows = db.all("""
SELECT (SELECT p FROM participants p WHERE p.id = sp.payer) AS payer
, json_agg((SELECT a FROM (
Expand Down Expand Up @@ -249,23 +260,20 @@ def send_upcoming_debit_notifications():
else:
event = 'missing_route'
payer.notify(event, email_unverified_address=True, **context)
counts[event] += 1
db.run("""
UPDATE scheduled_payins
SET notifs_count = notifs_count + 1
, last_notif_ts = now()
WHERE payer = %s
AND id IN %s
""", (payer.id, tuple(sp['id'] for sp in payins)))
for k, n in sorted(counts.items()):
logger.info("Sent %i %s notifications." % (n, k))


@log_notification_counts
def execute_scheduled_payins():
"""This daily cron job initiates scheduled payments.
"""
db = website.db
counts = defaultdict(int)
retry = False
rows = db.all("""
SELECT p AS payer, json_agg(json_build_object(
Expand Down Expand Up @@ -364,7 +372,6 @@ def unpack():
email_unverified_address=True,
force_email=True,
)
counts['renewal_unauthorized'] += 1
continue
if payin.status == 'failed' and route.status == 'expired':
can_retry = db.one("""
Expand All @@ -378,14 +385,6 @@ def unpack():
if can_retry:
retry = True
continue
if payin.status in ('failed', 'succeeded'):
payer.notify(
'payin_' + payin.status,
payin=payin._asdict(),
provider='Stripe',
email_unverified_address=True,
)
counts['payin_' + payin.status] += 1
elif actionable:
db.run("""
UPDATE scheduled_payins
Expand All @@ -406,7 +405,6 @@ def unpack():
email_unverified_address=True,
force_email=True,
)
counts['renewal_actionable'] += 1
if impossible:
for tr in impossible:
tr['execution_date'] = execution_date
Expand All @@ -416,9 +414,6 @@ def unpack():
transfers=impossible,
email_unverified_address=True,
)
counts['renewal_aborted'] += 1
for k, n in sorted(counts.items()):
logger.info("Sent %i %s notifications." % (n, k))
if retry:
execute_scheduled_payins()

Expand Down
8 changes: 6 additions & 2 deletions liberapay/payin/paypal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from ..i18n.currencies import Money
from ..website import website
from .common import (
abort_payin, update_payin, update_payin_transfer, record_payin_refund,
record_payin_transfer_reversal,
abort_payin, handle_payin_result, update_payin, update_payin_transfer,
record_payin_refund, record_payin_transfer_reversal,
)


Expand Down Expand Up @@ -243,6 +243,7 @@ def record_order_result(db, payin, order):
)
for pu in order['purchase_units']
) or None
old_status = payin.status
payin = update_payin(
db, payin.id, order['id'], status, error, refunded_amount=refunded_amount
)
Expand Down Expand Up @@ -286,6 +287,9 @@ def record_order_result(db, payin, order):
amount=net_amount, fee=pt_fee, reversed_amount=reversed_amount
)

if payin.status != old_status and payin.status in ('failed', 'succeeded'):
handle_payin_result(db, payin)

return payin


Expand Down
Loading
Loading