From 4e3a12ee8a964b4d126cd717c6ac988ee677f568 Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 28 Oct 2024 10:07:25 +0100 Subject: [PATCH 1/2] refactor checking for Stripe SEPA accounts --- liberapay/models/participant.py | 77 ++++++++++++------------ www/%username/giving/pay/%payment_id.spt | 32 +--------- 2 files changed, 41 insertions(+), 68 deletions(-) diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index 1eea50ec5..aae76c7f7 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -3366,43 +3366,7 @@ def group_tips_into_payments(self, tips): groups['self_donation'].append(tip) else: n_fundable += 1 - if tippee_p.payment_providers & 1 == 1: - members = set(members) - members.discard(self.id) - in_sepa = self.db.one(""" - SELECT true - FROM current_takes t - JOIN payment_accounts a ON a.participant = t.member - WHERE t.team = %(tippee)s - AND t.member IN %(members)s - AND a.provider = 'stripe' - AND a.is_current - AND a.charges_enabled - AND a.country IN %(SEPA)s - LIMIT 1 - """, dict(members=members, tippee=tip.tippee, SEPA=SEPA)) - if in_sepa: - group = stripe_europe.setdefault(tip.amount.currency, []) - if len(group) == 0: - groups['fundable'].append(group) - group.append(tip) - else: - groups['fundable'].append([tip]) - else: - groups['fundable'].append([tip]) - else: - n_fundable += 1 - if tippee_p.payment_providers & 1 == 1: - in_sepa = self.db.one(""" - SELECT true - FROM payment_accounts a - WHERE a.participant = %(tippee)s - AND a.provider = 'stripe' - AND a.is_current - AND a.charges_enabled - AND a.country IN %(SEPA)s - LIMIT 1 - """, dict(tippee=tip.tippee, SEPA=SEPA)) + in_sepa = tip.tippee_p.has_stripe_sepa_for(self) if in_sepa: group = stripe_europe.setdefault(tip.amount.currency, []) if len(group) == 0: @@ -3410,10 +3374,49 @@ def group_tips_into_payments(self, tips): group.append(tip) else: groups['fundable'].append([tip]) + else: + n_fundable += 1 + in_sepa = tip.tippee_p.has_stripe_sepa_for(self) + if in_sepa: + group = stripe_europe.setdefault(tip.amount.currency, []) + if len(group) == 0: + groups['fundable'].append(group) + group.append(tip) else: groups['fundable'].append([tip]) return groups, n_fundable + def has_stripe_sepa_for(self, tipper): + if tipper == self or self.payment_providers & 1 == 0: + return False + if self.kind == 'group': + return self.db.one(""" + SELECT true + FROM current_takes t + JOIN participants p ON p.id = t.member + JOIN payment_accounts a ON a.participant = t.member + WHERE t.team = %(tippee)s + AND t.member <> %(tipper)s + AND t.amount <> 0 + AND p.is_suspended IS NOT TRUE + AND a.provider = 'stripe' + AND a.is_current + AND a.charges_enabled + AND a.country IN %(SEPA)s + LIMIT 1 + """, dict(tipper=tipper.id, tippee=self.id, SEPA=SEPA)) + else: + return self.db.one(""" + SELECT true + FROM payment_accounts a + WHERE a.participant = %(tippee)s + AND a.provider = 'stripe' + AND a.is_current + AND a.charges_enabled + AND a.country IN %(SEPA)s + LIMIT 1 + """, dict(tippee=self.id, SEPA=SEPA)) + def get_tips_to(self, tippee_ids): return self.db.all(""" SELECT t.*, p AS tippee_p diff --git a/www/%username/giving/pay/%payment_id.spt b/www/%username/giving/pay/%payment_id.spt index 3f543a67b..a21213aa5 100644 --- a/www/%username/giving/pay/%payment_id.spt +++ b/www/%username/giving/pay/%payment_id.spt @@ -2,7 +2,6 @@ from functools import reduce from math import ceil from types import SimpleNamespace -from liberapay.constants import SEPA from liberapay.i18n.base import BOLD from liberapay.models.participant import Participant from liberapay.utils import get_participant, group_by, partition @@ -15,35 +14,6 @@ allow_stripe_card = website.app_conf.payin_methods.get('stripe-card', True) allow_stripe_sdd = website.app_conf.payin_methods.get('stripe-sdd', True) allow_paypal = website.app_conf.payin_methods.get('paypal', True) -def is_sepa(payment): - tip = payment.tips[0] - if tip.tippee_p.kind == 'group': - return website.db.one(""" - SELECT true - FROM current_takes t - JOIN participants p ON p.id = t.member - JOIN payment_accounts a ON a.participant = t.member - WHERE t.team = %(tippee)s - AND t.amount <> 0 - AND p.is_suspended IS NOT TRUE - AND a.provider = 'stripe' - AND a.is_current - AND a.charges_enabled - AND a.country IN %(SEPA)s - LIMIT 1 - """, dict(tippee=tip.tippee, SEPA=SEPA), default=False) - else: - return website.db.one(""" - SELECT true - FROM payment_accounts a - WHERE a.participant = %(tippee)s - AND a.provider = 'stripe' - AND a.is_current - AND a.charges_enabled - AND a.country IN %(SEPA)s - LIMIT 1 - """, dict(tippee=tip.tippee, SEPA=SEPA), default=False) - [---] payer = get_participant(state, restrict=True) @@ -306,7 +276,7 @@ title = _("Funding your donations") % endif - % set sepa = is_sepa(payment) + % set sepa = payment.tips[0].tippee_p.has_stripe_sepa_for(payer) % set possible = allow_stripe_sdd and sepa and payment.currency == 'EUR' % set warn = payer.guessed_country not in constants.SEPA
From 0eedd0c473dc48fe72d304846d529d20c9fed90e Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 17 Oct 2024 17:37:36 +0200 Subject: [PATCH 2/2] fix `send_upcoming_debit_notifications` --- liberapay/payin/cron.py | 107 +++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/liberapay/payin/cron.py b/liberapay/payin/cron.py index 89d0e0636..554e4ac00 100644 --- a/liberapay/payin/cron.py +++ b/liberapay/payin/cron.py @@ -201,9 +201,9 @@ def send_upcoming_debit_notifications(): AND sp.notifs_count = 0 AND sp.payin IS NULL AND sp.ctime < (current_timestamp - interval '6 hours') - GROUP BY sp.payer, (sp.amount).currency + GROUP BY sp.payer HAVING min(sp.execution_date) <= (current_date + interval '14 days') - ORDER BY sp.payer, (sp.amount).currency + ORDER BY sp.payer """) for payer, payins in rows: if not payer.can_attempt_payment: @@ -211,53 +211,68 @@ def send_upcoming_debit_notifications(): _check_scheduled_payins(db, payer, payins, automatic=True) if not payins: continue - payins.sort(key=itemgetter('execution_date')) - context = { - 'payins': payins, - 'total_amount': sum(sp['amount'] for sp in payins), - } - for sp in context['payins']: + payins.sort(key=lambda sp: ( + sp['amount'].currency, sp['execution_date'], sp['id'] + )) + routes = website.db.all(""" + SELECT r + FROM exchange_routes r + WHERE r.participant = %(payer_id)s + AND r.status = 'chargeable' + AND r.network::text LIKE 'stripe-%%' + """, dict(payer_id=payer.id)) + for route in routes: + route.__dict__['participant'] = payer + grouped_payins = defaultdict(list) + for sp in payins: for tr in sp['transfers']: del tr['tip'], tr['beneficiary'] - if len(payins) > 1: - last_execution_date = payins[-1]['execution_date'] - max_execution_date = max(sp['execution_date'] for sp in payins) - assert last_execution_date == max_execution_date - context['ndays'] = (max_execution_date - utcnow().date()).days - currency = payins[0]['amount'].currency - while True: - route = db.one(""" - SELECT r - FROM exchange_routes r - WHERE r.participant = %s - AND r.status = 'chargeable' - AND r.network::text LIKE 'stripe-%%' - ORDER BY r.is_default_for = %s DESC NULLS LAST - , r.is_default NULLS LAST - , r.network = 'stripe-sdd' DESC - , r.ctime DESC - LIMIT 1 - """, (payer.id, currency)) - if route is None: - break - route.sync_status() - if route.status == 'chargeable': + currency = sp['amount'].currency + routes.sort(key=lambda r: ( + -(r.is_default_for == currency), + -(r.is_default is True), + -(r.network == 'stripe-sdd'), + -(r.ctime.timestamp()), + )) + recipients_are_in_sepa = ( + len(sp['transfers']) > 1 or db.Participant.from_id( + sp['transfers'][0]['tippee_id'] + ).has_stripe_sepa_for(payer) + ) + suitable_route = None + for route in routes: + if route.network == 'stripe-sdd' and not recipients_are_in_sepa: + continue + suitable_route = route break - if route: - event = 'upcoming_debit' - context['instrument_brand'] = route.get_brand() - context['instrument_partial_number'] = route.get_partial_number() - 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))) + grouped_payins[(currency, suitable_route)].append(sp) + del suitable_route + del payins + for (currency, route), payins in grouped_payins.items(): + context = { + 'payins': payins, + 'total_amount': sum(sp['amount'] for sp in payins), + } + if len(payins) > 1: + last_execution_date = payins[-1]['execution_date'] + max_execution_date = max(sp['execution_date'] for sp in payins) + assert last_execution_date == max_execution_date + context['ndays'] = (max_execution_date - utcnow().date()).days + if route: + event = 'upcoming_debit' + context['instrument_brand'] = route.get_brand() + context['instrument_partial_number'] = route.get_partial_number() + 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))