From ba0e4576b05c525381113d07d9e0f818778c9b4c Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 3 Sep 2023 14:47:52 +0200 Subject: [PATCH 01/13] handle stripe permission error during refresh fixes LIBERAPAYCOM-21F --- www/%username/payment/index.spt | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/www/%username/payment/index.spt b/www/%username/payment/index.spt index 875b5d2e4..00aec0fda 100644 --- a/www/%username/payment/index.spt +++ b/www/%username/payment/index.spt @@ -9,6 +9,7 @@ participant = get_participant(state, restrict=True) if request.method == 'POST': account_pk = request.body.get_int('account_pk') action = request.body.get_choice('action', ('disconnect', 'refresh'), default='disconnect') + msg = '' if action == 'disconnect': account = website.db.one(""" UPDATE payment_accounts @@ -27,6 +28,7 @@ if request.method == 'POST': ) if not expected: website.warning("unexpected error message: " + str(e)) + msg = _("The payment account has been successfully disconnected.") elif action == 'refresh': account = website.db.one(""" SELECT * @@ -37,25 +39,36 @@ if request.method == 'POST': if not account: raise response.invalid_input(account_pk, 'account_pk', 'body') if account.provider == 'stripe': - stripe_account = stripe.Account.retrieve(account.id) - website.db.run(""" - UPDATE payment_accounts - SET country = %(country)s - , default_currency = %(default_currency)s - , charges_enabled = %(charges_enabled)s - , display_name = %(display_name)s - WHERE provider = 'stripe' - AND id = %(account_id)s - """, dict( - country=stripe_account.country, - default_currency=stripe_account.default_currency.upper(), - charges_enabled=stripe_account.charges_enabled, - display_name=stripe_account.settings.dashboard.display_name, - account_id=stripe_account.id, - )) + try: + stripe_account = stripe.Account.retrieve(account.id) + except stripe.error.PermissionError as e: + website.db.run(""" + UPDATE payment_accounts + SET is_current = null + , authorized = false + WHERE id = %s + """, (account.id,)) + msg = _("This payment account is no longer accessible. It is now disconnected.") + else: + website.db.run(""" + UPDATE payment_accounts + SET country = %(country)s + , default_currency = %(default_currency)s + , charges_enabled = %(charges_enabled)s + , display_name = %(display_name)s + WHERE provider = 'stripe' + AND id = %(account_id)s + """, dict( + country=stripe_account.country, + default_currency=stripe_account.default_currency.upper(), + charges_enabled=stripe_account.charges_enabled, + display_name=stripe_account.settings.dashboard.display_name, + account_id=stripe_account.id, + )) + msg = _("The data has been successfully refreshed.") else: raise response.error(400, f"refresh isn't implemented for provider {account.provider}") - response.redirect(request.path.raw) + form_post_success(state, msg=msg, redirect_url=request.path.raw) accounts = website.db.all(""" SELECT * From ec2d8c444a6f54f87c1b9c7c9116a37987a2c95e Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 4 Sep 2023 10:36:13 +0200 Subject: [PATCH 02/13] tighten the `Referrer-Policy` during authentication https://hackerone.com/reports/2133308 --- liberapay/security/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/liberapay/security/authentication.py b/liberapay/security/authentication.py index 79a94e186..83ea6e0bd 100644 --- a/liberapay/security/authentication.py +++ b/liberapay/security/authentication.py @@ -327,6 +327,7 @@ def authenticate_user_if_possible(csrf_token, request, response, state, user, _) )[0] if p: if p.id != user.id: + response.headers[b'Referrer-Policy'] = b'strict-origin' submitted_confirmation_token = request.qs.get('log-in.confirmation') if submitted_confirmation_token: expected_confirmation_token = b64encode_s(blake2b( From f9a3dd72231e0e1f59807151a68dcbc6f211fdcd Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 4 Sep 2023 11:07:03 +0200 Subject: [PATCH 03/13] improve handling of "zero-decimal" currencies --- liberapay/constants.py | 11 ++----- liberapay/i18n/currencies.py | 31 ++++++++++++++----- liberapay/payin/stripe.py | 11 ++----- liberapay/utils/__init__.py | 3 +- liberapay/utils/fake_data.py | 4 +-- www/%username/giving/pay/paypal/%payin_id.spt | 1 + www/%username/giving/pay/stripe/%payin_id.spt | 1 + 7 files changed, 36 insertions(+), 26 deletions(-) diff --git a/liberapay/constants.py b/liberapay/constants.py index 5354ea103..eef505c49 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -3,13 +3,10 @@ from decimal import Decimal, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP import re -from babel.numbers import get_currency_precision from markupsafe import Markup from pando.utils import utc -from .i18n.currencies import ( # noqa: F401 - CURRENCIES, CURRENCY_REPLACEMENTS, D_CENT, D_ZERO, D_MAX, Money, -) +from .i18n.currencies import CURRENCIES, D_CENT, Money # noqa: F401 def check_bits(bits): @@ -385,13 +382,11 @@ def __missing__(self, currency): def make_standard_tip(label, weekly, currency): - precision = get_currency_precision(currency) - minimum = D_CENT if precision == 2 else Decimal(10) ** (-precision) return StandardTip( label, Money(weekly, currency), - Money((weekly / PERIOD_CONVERSION_RATES['monthly']).quantize(minimum), currency), - Money((weekly / PERIOD_CONVERSION_RATES['yearly']).quantize(minimum), currency), + Money((weekly / PERIOD_CONVERSION_RATES['monthly']), currency, rounding=ROUND_HALF_UP), + Money((weekly / PERIOD_CONVERSION_RATES['yearly']), currency, rounding=ROUND_HALF_UP), ) diff --git a/liberapay/i18n/currencies.py b/liberapay/i18n/currencies.py index b20f65110..8183ca38d 100644 --- a/liberapay/i18n/currencies.py +++ b/liberapay/i18n/currencies.py @@ -1,10 +1,9 @@ from datetime import datetime from decimal import Decimal, InvalidOperation, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP -from itertools import starmap, zip_longest +from itertools import chain, starmap, zip_longest from numbers import Number import operator -from babel.numbers import get_currency_precision from pando.utils import utc import requests import xmltodict @@ -24,9 +23,23 @@ 'HRK': (Decimal('7.53450'), 'EUR', datetime(2023, 1, 1, 1, 0, 0, tzinfo=utc)), } +ZERO_DECIMAL_CURRENCIES = { + # https://developer.paypal.com/reference/currency-codes/ + 'paypal': {'HUF', 'JPY', 'TWD'}, + # https://stripe.com/docs/currencies#presentment-currencies + 'stripe': { + 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', + 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF', + }, +} +ZERO_DECIMAL_CURRENCIES['any'] = set(chain(*ZERO_DECIMAL_CURRENCIES.values())) + + D_CENT = Decimal('0.01') D_MAX = Decimal('999999999999.99') -D_ZERO = Decimal('0.00') +D_ONE = Decimal('1') +D_ZERO = Decimal('0') +D_ZERO_CENT = Decimal('0.00') class CurrencyMismatch(ValueError): @@ -35,15 +48,19 @@ class CurrencyMismatch(ValueError): class _Minimums(dict): def __missing__(self, currency): - exponent = get_currency_precision(currency) - minimum = Money((D_CENT if exponent == 2 else Decimal(10) ** (-exponent)), currency) + minimum = Money( + D_ONE if currency in ZERO_DECIMAL_CURRENCIES['any'] else D_CENT, + currency + ) self[currency] = minimum return minimum class _Zeros(dict): def __missing__(self, currency): - minimum = Money.MINIMUMS[currency].amount - zero = Money((D_ZERO if minimum is D_CENT else minimum - minimum), currency) + zero = Money( + D_ZERO if currency in ZERO_DECIMAL_CURRENCIES['any'] else D_ZERO_CENT, + currency + ) self[currency] = zero return zero diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py index a444d03f6..39276477d 100644 --- a/liberapay/payin/stripe.py +++ b/liberapay/payin/stripe.py @@ -6,7 +6,7 @@ from ..constants import EPOCH, PAYIN_SETTLEMENT_DELAYS, SEPA from ..exceptions import MissingPaymentAccount, NextAction, NoSelfTipping -from ..i18n.currencies import Money +from ..i18n.currencies import Money, ZERO_DECIMAL_CURRENCIES from ..models.exchange_route import ExchangeRoute from ..website import website from .common import ( @@ -23,21 +23,16 @@ 'requested_by_customer': 'requested_by_payer', } -# https://stripe.com/docs/currencies#presentment-currencies -ZERO_DECIMAL_CURRENCIES = """ - BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF -""".split() - def int_to_Money(amount, currency): currency = currency.upper() - if currency in ZERO_DECIMAL_CURRENCIES: + if currency in ZERO_DECIMAL_CURRENCIES['stripe']: return Money(Decimal(amount), currency) return Money(Decimal(amount) / 100, currency) def Money_to_int(m): - if m.currency in ZERO_DECIMAL_CURRENCIES: + if m.currency in ZERO_DECIMAL_CURRENCIES['stripe']: return int(m.amount) return int(m.amount * 100) diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py index 1a678ca68..bb54da4bc 100644 --- a/liberapay/utils/__init__.py +++ b/liberapay/utils/__init__.py @@ -14,13 +14,14 @@ from pando.utils import to_rfc822, utcnow from markupsafe import Markup -from liberapay.constants import CURRENCIES, CURRENCY_REPLACEMENTS, SAFE_METHODS +from liberapay.constants import SAFE_METHODS from liberapay.elsewhere._paginators import _modify_query from liberapay.exceptions import ( AuthRequired, ClosedAccount, LoginRequired, TooManyAdminActions, ) from liberapay.models.community import Community from liberapay.i18n.base import LOCALE_EN, add_helpers_to_context +from liberapay.i18n.currencies import CURRENCIES, CURRENCY_REPLACEMENTS from liberapay.website import website from liberapay.utils import cbor diff --git a/liberapay/utils/fake_data.py b/liberapay/utils/fake_data.py index c143e0df5..3f03cd4f3 100644 --- a/liberapay/utils/fake_data.py +++ b/liberapay/utils/fake_data.py @@ -5,9 +5,9 @@ from faker import Faker from psycopg2 import IntegrityError -from liberapay.constants import D_CENT, DONATION_LIMITS, PERIOD_CONVERSION_RATES +from liberapay.constants import DONATION_LIMITS, PERIOD_CONVERSION_RATES from liberapay.exceptions import CommunityAlreadyExists -from liberapay.i18n.currencies import Money +from liberapay.i18n.currencies import D_CENT, Money from liberapay.models import community diff --git a/www/%username/giving/pay/paypal/%payin_id.spt b/www/%username/giving/pay/paypal/%payin_id.spt index 5b5582989..2fbf68d38 100644 --- a/www/%username/giving/pay/paypal/%payin_id.spt +++ b/www/%username/giving/pay/paypal/%payin_id.spt @@ -32,6 +32,7 @@ if request.method == 'POST': payin_amount = Money(payin_amount, payin_currency) payin_amount = payin_amount.convert_if_currency_is_phased_out() del payin_currency + payin_amount = payin_amount.round() tips = website.db.all(""" SELECT t.*, p AS tippee_p diff --git a/www/%username/giving/pay/stripe/%payin_id.spt b/www/%username/giving/pay/stripe/%payin_id.spt index e1db9b691..3c12a6e6b 100644 --- a/www/%username/giving/pay/stripe/%payin_id.spt +++ b/www/%username/giving/pay/stripe/%payin_id.spt @@ -41,6 +41,7 @@ if request.method == 'POST': payin_amount = Money(payin_amount, payin_currency) payin_amount = payin_amount.convert_if_currency_is_phased_out() del payin_currency + payin_amount = payin_amount.round() tips = website.db.all(""" SELECT t.*, p AS tippee_p From 5feabf8717d470a9dda9dfa60e72e05c17eff5ca Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 6 Sep 2023 17:36:13 +0200 Subject: [PATCH 04/13] call the renewal scheduler less often --- liberapay/payin/cron.py | 36 ++++++++---- liberapay/payin/stripe.py | 115 +++++++++++++++++++++++++++----------- www/callbacks/stripe.spt | 5 +- 3 files changed, 111 insertions(+), 45 deletions(-) diff --git a/liberapay/payin/cron.py b/liberapay/payin/cron.py index 50eeac7aa..e27fb00d5 100644 --- a/liberapay/payin/cron.py +++ b/liberapay/payin/cron.py @@ -14,6 +14,7 @@ from ..i18n.currencies import Money from ..website import website from ..utils import utcnow +from ..utils.types import Object from .common import prepare_payin, resolve_tip from .stripe import charge @@ -253,8 +254,12 @@ def execute_scheduled_payins(): counts = defaultdict(int) retry = False rows = db.all(""" - SELECT sp.id, sp.execution_date, sp.transfers - , p AS payer, r.*::exchange_routes AS route + SELECT p AS payer, json_agg(json_build_object( + 'id', sp.id, + 'execution_date', sp.execution_date, + 'transfers', sp.transfers, + 'route', r.id + )) AS scheduled_payins FROM scheduled_payins sp JOIN participants p ON p.id = sp.payer JOIN LATERAL ( @@ -275,14 +280,25 @@ def execute_scheduled_payins(): AND sp.automatic AND sp.payin IS NULL AND p.is_suspended IS NOT TRUE - ORDER BY sp.execution_date, sp.id + GROUP BY p.id + ORDER BY p.id """) - for sp_id, execution_date, transfers, payer, route in rows: - route.__dict__['participant'] = payer - route.sync_status() - if route.status != 'chargeable': - retry = True - continue + for payer, scheduled_payins in rows: + scheduled_payins[:] = [Object(**sp) for sp in scheduled_payins] + for sp in scheduled_payins: + sp.route = db.ExchangeRoute.from_id(payer, sp.route) + sp.route.sync_status() + if sp.route.status != 'chargeable': + retry = True + scheduled_payins.remove(sp) + + def unpack(): + for payer, scheduled_payins in rows: + last = len(scheduled_payins) + for i, sp in enumerate(scheduled_payins, 1): + yield sp.id, sp.execution_date, sp.transfers, payer, sp.route, i == last + + for sp_id, execution_date, transfers, payer, route, update_donor in unpack(): transfers, canceled, impossible, actionable = _filter_transfers( payer, transfers, automatic=True ) @@ -325,7 +341,7 @@ def execute_scheduled_payins(): WHERE id = %s """, (payin.id, sp_id)) try: - payin = charge(db, payin, payer, route) + payin = charge(db, payin, payer, route, update_donor=update_donor) except NextAction: payer.notify( 'renewal_unauthorized', diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py index 39276477d..a54ab9084 100644 --- a/liberapay/payin/stripe.py +++ b/liberapay/payin/stripe.py @@ -80,7 +80,7 @@ def create_source_from_token(token_id, one_off, amount, owner_info, return_url): ) -def charge(db, payin, payer, route): +def charge(db, payin, payer, route, update_donor=True): """Initiate the Charge for the given payin. Returns the updated payin, or possibly a new payin. @@ -111,7 +111,7 @@ def charge(db, payin, payer, route): else: new_payin_error = 'canceled' if new_status == 'failed' else None payin = update_payin(db, payin.id, None, new_status, new_payin_error) - for pt in transfers: + for i, pt in enumerate(transfers, 1): new_transfer_error = ( "canceled because payer account is blocked" if payer.is_suspended else @@ -119,24 +119,31 @@ def charge(db, payin, payer, route): if pt.recipient_marked_as in ('fraud', 'spam') else "canceled because another destination account is blocked" ) if new_status == 'failed' else None - update_payin_transfer(db, pt.id, None, new_status, new_transfer_error) + update_payin_transfer( + db, pt.id, None, new_status, new_transfer_error, + update_donor=(update_donor and i == len(transfers)), + ) return payin if len(transfers) == 1: payin, charge = destination_charge( - db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id) + db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id), + update_donor=update_donor, ) if payin.status == 'failed': - payin, charge = try_other_destinations(db, payin, payer, charge) + payin, charge = try_other_destinations( + db, payin, payer, charge, update_donor=update_donor, + ) else: payin, charge = charge_and_transfer( - db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id) + db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id), + update_donor=update_donor, ) if charge and charge.status == 'failed' and charge.failure_code == 'expired_card': route.update_status('expired') return payin -def try_other_destinations(db, payin, payer, charge): +def try_other_destinations(db, payin, payer, charge, update_donor=True): """Retry a failed charge with different destinations. Returns a payin. @@ -189,11 +196,13 @@ def try_other_destinations(db, payin, payer, charge): ) if len(payin_transfers) == 1: payin, charge = destination_charge( - db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id) + db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id), + update_donor=update_donor, ) else: payin, charge = charge_and_transfer( - db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id) + db, payin, payer, statement_descriptor=('Liberapay %i' % payin.id), + update_donor=update_donor, ) except NextAction: raise @@ -210,7 +219,9 @@ def try_other_destinations(db, payin, payer, charge): return payin, charge -def charge_and_transfer(db, payin, payer, statement_descriptor, on_behalf_of=None): +def charge_and_transfer( + db, payin, payer, statement_descriptor, on_behalf_of=None, update_donor=True, +): """Create a standalone Charge then multiple Transfers. Doc: https://stripe.com/docs/connect/charges-transfers @@ -266,12 +277,14 @@ def charge_and_transfer(db, payin, payer, statement_descriptor, on_behalf_of=Non else: charge = intent.charges.data[0] intent_id = getattr(intent, 'id', None) - payin = settle_charge_and_transfers(db, payin, charge, intent_id=intent_id) + payin = settle_charge_and_transfers( + db, payin, charge, intent_id=intent_id, update_donor=update_donor, + ) send_payin_notification(db, payin, payer, charge, route) return payin, charge -def destination_charge(db, payin, payer, statement_descriptor): +def destination_charge(db, payin, payer, statement_descriptor, update_donor=True): """Create a Destination Charge. Doc: https://stripe.com/docs/connect/destination-charges @@ -335,7 +348,9 @@ def destination_charge(db, payin, payer, statement_descriptor): else: charge = intent.charges.data[0] intent_id = getattr(intent, 'id', None) - payin = settle_destination_charge(db, payin, charge, pt, intent_id=intent_id) + payin = settle_destination_charge( + db, payin, charge, pt, intent_id=intent_id, update_donor=update_donor, + ) send_payin_notification(db, payin, payer, charge, route) return payin, charge @@ -401,7 +416,9 @@ def settle_charge(db, payin, charge): return settle_charge_and_transfers(db, payin, charge) -def settle_charge_and_transfers(db, payin, charge, intent_id=None): +def settle_charge_and_transfers( + db, payin, charge, intent_id=None, update_donor=True, +): """Record the result of a charge, and execute the transfers if it succeeded. """ if getattr(charge, 'balance_transaction', None): @@ -438,18 +455,31 @@ def settle_charge_and_transfers(db, payin, charge, intent_id=None): WHERE pt.payin = %s ORDER BY pt.id """, (payin.id,)) + last = len(payin_transfers) - 1 if amount_settled is not None: payer = db.Participant.from_id(payin.payer) undeliverable_amount = amount_settled.zero() for i, pt in enumerate(payin_transfers): if payer.is_suspended and pt.status not in ('failed', 'succeeded'): - pt = update_payin_transfer(db, pt.id, None, 'suspended', None) + pt = update_payin_transfer( + db, pt.id, None, 'suspended', None, + update_donor=(update_donor and i == last), + ) elif pt.destination_id == 'acct_1ChyayFk4eGpfLOC': - pt = update_payin_transfer(db, pt.id, None, charge.status, error) + pt = update_payin_transfer( + db, pt.id, None, charge.status, error, + update_donor=(update_donor and i == last), + ) elif pt.remote_id is None and pt.status in ('pre', 'pending'): - pt = execute_transfer(db, pt, pt.destination_id, charge.id) + pt = execute_transfer( + db, pt, pt.destination_id, charge.id, + update_donor=(update_donor and i == last), + ) elif payin.refunded_amount and pt.remote_id: - pt = sync_transfer(db, pt) + pt = sync_transfer( + db, pt, + update_donor=(update_donor and i == last), + ) if pt.status == 'failed': undeliverable_amount += pt.amount payin_transfers[i] = pt @@ -479,17 +509,21 @@ def settle_charge_and_transfers(db, payin, charge, intent_id=None): for i, pt in enumerate(payin_transfers): if pt.status == 'succeeded': payin_transfers[i] = reverse_transfer( - db, pt, payin_refund_id=payin_refund_id + db, pt, payin_refund_id=payin_refund_id, + update_donor=(update_donor and i == last), ) elif charge.status in ('failed', 'pending'): - for pt in payin_transfers: - update_payin_transfer(db, pt.id, None, charge.status, error) + for i, pt in enumerate(payin_transfers): + update_payin_transfer( + db, pt.id, None, charge.status, error, + update_donor=(update_donor and i == last), + ) return payin -def execute_transfer(db, pt, destination, source_transaction): +def execute_transfer(db, pt, destination, source_transaction, update_donor=True): """Create a Transfer. Args: @@ -536,16 +570,24 @@ def execute_transfer(db, pt, destination, source_transaction): if alternate_destination: return execute_transfer(db, pt, alternate_destination, source_transaction) error = "The recipient's account no longer exists." - return update_payin_transfer(db, pt.id, None, 'failed', error) + return update_payin_transfer( + db, pt.id, None, 'failed', error, update_donor=update_donor, + ) else: website.tell_sentry(e, allow_reraise=False) - return update_payin_transfer(db, pt.id, None, 'pending', error) + return update_payin_transfer( + db, pt.id, None, 'pending', error, update_donor=update_donor, + ) except Exception as e: website.tell_sentry(e) - return update_payin_transfer(db, pt.id, None, 'pending', str(e)) + return update_payin_transfer( + db, pt.id, None, 'pending', str(e), update_donor=update_donor, + ) # `Transfer` objects don't have a `status` attribute, so if no exception was # raised we assume that the transfer was successful. - pt = update_payin_transfer(db, pt.id, tr.id, 'succeeded', None) + pt = update_payin_transfer( + db, pt.id, tr.id, 'succeeded', None, update_donor=update_donor, + ) update_transfer_metadata(tr, pt) return pt @@ -588,7 +630,10 @@ def refund_payin(db, payin, refund_amount=None): ) -def reverse_transfer(db, pt, reversal_amount=None, payin_refund_id=None, idempotency_key=None): +def reverse_transfer( + db, pt, reversal_amount=None, payin_refund_id=None, idempotency_key=None, + update_donor=True, +): """Create a Transfer Reversal. Args: @@ -620,7 +665,7 @@ def reverse_transfer(db, pt, reversal_amount=None, payin_refund_id=None, idempot if str(e).endswith(" is already fully reversed."): return update_payin_transfer( db, pt.id, pt.remote_id, pt.status, pt.error, - reversed_amount=pt.amount, + reversed_amount=pt.amount, update_donor=update_donor, ) else: raise @@ -630,11 +675,12 @@ def reverse_transfer(db, pt, reversal_amount=None, payin_refund_id=None, idempot ctime=(EPOCH + timedelta(seconds=reversal.created)), ) return update_payin_transfer( - db, pt.id, pt.remote_id, pt.status, pt.error, reversed_amount=new_reversed_amount + db, pt.id, pt.remote_id, pt.status, pt.error, reversed_amount=new_reversed_amount, + update_donor=update_donor, ) -def sync_transfer(db, pt): +def sync_transfer(db, pt, update_donor=True): """Fetch the transfer's data and update our database. Args: @@ -653,11 +699,14 @@ def sync_transfer(db, pt): reversed_amount = None record_reversals(db, pt, tr) return update_payin_transfer( - db, pt.id, tr.id, 'succeeded', None, reversed_amount=reversed_amount + db, pt.id, tr.id, 'succeeded', None, reversed_amount=reversed_amount, + update_donor=update_donor, ) -def settle_destination_charge(db, payin, charge, pt, intent_id=None): +def settle_destination_charge( + db, payin, charge, pt, intent_id=None, update_donor=True, +): """Record the result of a charge, and recover the fee. """ if getattr(charge, 'balance_transaction', None): @@ -703,7 +752,7 @@ def settle_destination_charge(db, payin, charge, pt, intent_id=None): pt_remote_id = getattr(charge, 'transfer', None) pt = update_payin_transfer( db, pt.id, pt_remote_id, status, error, amount=net_amount, - reversed_amount=reversed_amount, + reversed_amount=reversed_amount, update_donor=update_donor, ) return payin diff --git a/www/callbacks/stripe.spt b/www/callbacks/stripe.spt index 9ac73ff61..976ebdaf2 100644 --- a/www/callbacks/stripe.spt +++ b/www/callbacks/stripe.spt @@ -74,14 +74,15 @@ elif event_object_type == 'charge.dispute': WHERE pt.payin = %s AND coalesce(pt.reversed_amount < pt.amount, true) """, (payin.id,)) - for pt in transfers: + for i, pt in enumerate(transfers, 1): if pt.status == 'succeeded': reverse_transfer( website.db, pt, idempotency_key=f'{dispute.id}_pt_{pt.id}', ) elif pt.status != 'failed': update_payin_transfer( - website.db, pt.id, None, 'failed', 'canceled due to chargeback' + website.db, pt.id, None, 'failed', 'canceled due to chargeback', + update_donor=(i == len(transfers)), ) # Notify the person who initiated the payment payer = Participant.from_id(payin.payer) From ec03f6e45d695145244ba5eb5e0c95f638401202 Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 11 Sep 2023 14:31:16 +0200 Subject: [PATCH 05/13] refine the list of countries supported by PayPal --- cli/paypal_payout_countries.py | 28 ++++++++++++++++++++++++++++ liberapay/constants.py | 16 ++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 cli/paypal_payout_countries.py diff --git a/cli/paypal_payout_countries.py b/cli/paypal_payout_countries.py new file mode 100644 index 000000000..1a4539f6b --- /dev/null +++ b/cli/paypal_payout_countries.py @@ -0,0 +1,28 @@ +import re +from time import sleep + +import requests + + +sess = requests.Session() +r = sess.get('https://www.paypal.com/webapps/mpp/country-worldwide') +country_codes = set(re.findall(r"/([a-z]{2})/home", r.text)) +for cc in sorted(country_codes): + print(f"Requesting info for country code {cc.upper()}") + r = sess.get(f"https://www.paypal.com/{cc}/home") + if "Please wait while we perform security check" in r.text: + raise Exception("PayPal blocked the request") + if f"/{cc}/webapps/" not in r.text: + raise Exception("PayPal's response doesn't seem to contain the expected information") + is_supported = ( + f"/{cc}/webapps/mpp/accept-payments-online" in r.text or + f"/{cc}/business/accept-payments" in r.text + ) + if not is_supported: + country_codes.remove(cc) + sleep(1.5) + +country_codes.remove('uk') +country_codes.add('gb') +print(f"PayPal should be available to creators in the following {len(country_codes)} countries:") +print(' '.join(map(str.upper, sorted(country_codes)))) diff --git a/liberapay/constants.py b/liberapay/constants.py index eef505c49..cd6fcddf8 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -275,17 +275,13 @@ def __missing__(self, currency): PAYOUT_COUNTRIES = { 'paypal': set(""" - AD AE AG AI AL AM AN AO AR AT AU AW AZ BA BB BE BF BG BH BI BJ BM BN BO - BR BS BT BW BY BZ C2 CA CD CG CH CI CK CL CM CO CR CV CY CZ DE DJ DK DM - DO DZ EC EE EG ER ES ET FI FJ FK FM FO FR GA GD GE GF GI GL GM GN GP GR - GT GW GY HK HN HR HU ID IE IL IN IS IT JM JO JP KE KG KH KI KM KN KR KW - KY KZ LA LC LI LK LS LT LU LV MA MC MD ME MG MH MK ML MN MQ MR MS MT MU - MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PL - PM PN PT PW PY QA RE RO RS RW SA SB SC SE SG SH SI SJ SK SL SM SN SO SR - ST SV SZ TC TD TG TH TJ TM TN TO TT TT TT TT TV TW TZ UA UG GB US UY VA - VC VE VG VN VU WF WS YE YT ZA ZM ZW + AD AE AG AL AR AT AU BA BB BE BG BH BM BR BS BW BZ CA CH CL CO CR CY CZ + DE DK DM DO DZ EC EE EG ES FI FJ FO FR GB GD GE GL GR GT HK HN HR HU ID + IE IL IN IS IT JM JO JP KE KN KR KW KY KZ LC LI LS LT LU LV MA MC MD MT + MU MW MX MY MZ NC NI NL NO NZ OM PA PE PF PH PL PT PW QA RO RS SA SC SE + SG SI SK SM SN SV TC TH TT TW US UY VE VN ZA PR - """.split()), # https://www.paypal.com/us/webapps/mpp/country-worldwide + """.split()), # see `cli/paypal_payout_countries.py` 'stripe': set(""" AT AU BE BG CA CH CY CZ DE DK EE ES FI FR GB GI GR HK HR HU IE IT JP LI From 6e4b7cf2e05fc05dc723345eeed87856a03dd8a0 Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 11 Sep 2023 18:25:03 +0200 Subject: [PATCH 06/13] fix information on supported countries --- i18n/core/ar.po | 14 +++++++------- i18n/core/ca.po | 6 +++--- i18n/core/cs.po | 8 ++++---- i18n/core/da.po | 6 +++--- i18n/core/de.po | 4 ++-- i18n/core/el.po | 4 ++-- i18n/core/eo.po | 4 ++-- i18n/core/es.po | 4 ++-- i18n/core/et.po | 4 ++-- i18n/core/fi.po | 4 ++-- i18n/core/fr.po | 4 ++-- i18n/core/fy.po | 4 ++-- i18n/core/ga.po | 4 ++-- i18n/core/hu.po | 4 ++-- i18n/core/id.po | 4 ++-- i18n/core/it.po | 4 ++-- i18n/core/ja.po | 4 ++-- i18n/core/ko.po | 4 ++-- i18n/core/lt.po | 4 ++-- i18n/core/lv.po | 4 ++-- i18n/core/ms.po | 4 ++-- i18n/core/nb.po | 4 ++-- i18n/core/nl.po | 6 +++--- i18n/core/pl.po | 8 ++++---- i18n/core/pt.po | 4 ++-- i18n/core/ro.po | 4 ++-- i18n/core/ru.po | 8 ++++---- i18n/core/sk.po | 6 +++--- i18n/core/sl.po | 4 ++-- i18n/core/sv.po | 4 ++-- i18n/core/tr.po | 6 +++--- i18n/core/uk.po | 8 ++++---- i18n/core/vi.po | 4 ++-- i18n/core/zh_Hans.po | 4 ++-- i18n/core/zh_Hant.po | 4 ++-- www/about/global.spt | 31 ++++++++++++++++++++++++------- www/about/payment-processors.spt | 2 +- 37 files changed, 113 insertions(+), 96 deletions(-) diff --git a/i18n/core/ar.po b/i18n/core/ar.po index c67a3f131..7a3210c39 100644 --- a/i18n/core/ar.po +++ b/i18n/core/ar.po @@ -4923,13 +4923,13 @@ msgstr "يدعم PayPal فقط {n_paypal_currencies} من العملات {n_libe #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." -msgstr[1] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." -msgstr[2] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." -msgstr[3] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." -msgstr[4] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." -msgstr[5] "يتوفر PayPal للمبدعين في أكثر من 200 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgstr[1] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgstr[2] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgstr[3] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgstr[4] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." +msgstr[5] "يتوفر PayPal للمبدعين في أكثر من 100 دولة ، بينما يدعم Stripe فقط البلدان {n} بطريقة مناسبة." #, fuzzy, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/ca.po b/i18n/core/ca.po index 3671242f5..765b5a789 100644 --- a/i18n/core/ca.po +++ b/i18n/core/ca.po @@ -4184,9 +4184,9 @@ msgstr "PayPal només admet {n_paypal_currencies} de les {n_liberapay_currencies #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal està disponible per als creadors de més de 200 països, mentre que Stripe només admet {n} país d'una manera adequada." -msgstr[1] "PayPal està disponible per als creadors de més de 200 països, mentre que Stripe només admet {n} països d'una manera adequada." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal està disponible per als creadors de més de 100 països, mentre que Stripe només admet {n} país d'una manera adequada." +msgstr[1] "PayPal està disponible per als creadors de més de 100 països, mentre que Stripe només admet {n} països d'una manera adequada." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/cs.po b/i18n/core/cs.po index 731fbea9d..9bc68eeed 100644 --- a/i18n/core/cs.po +++ b/i18n/core/cs.po @@ -4227,10 +4227,10 @@ msgstr "Služba PayPal podporuje pouze {n_paypal_currencies} z {n_liberapay_curr #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "Služba PayPal je k dispozici tvůrcům ve více než 200 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} zemi." -msgstr[1] "Služba PayPal je k dispozici tvůrcům ve více než 200 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} země." -msgstr[2] "Služba PayPal je k dispozici tvůrcům ve více než 200 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} zemí." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "Služba PayPal je k dispozici tvůrcům ve více než 100 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} zemi." +msgstr[1] "Služba PayPal je k dispozici tvůrcům ve více než 100 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} země." +msgstr[2] "Služba PayPal je k dispozici tvůrcům ve více než 100 zemích, zatímco Stripe vhodným způsobem podporuje pouze {n} zemí." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/da.po b/i18n/core/da.po index e04f0a822..52105b0c4 100644 --- a/i18n/core/da.po +++ b/i18n/core/da.po @@ -4183,9 +4183,9 @@ msgstr "PayPal understøtter kun {n_paypal_currencies} af de {n_liberapay_curren #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal er tilgængelig for skabere i mere end 200 lande, hvorimod Stripe kun understøtter {n} land på en passende måde." -msgstr[1] "PayPal er tilgængelig for skabere i mere end 200 lande, hvorimod Stripe kun understøtter {n} lande på en passende måde." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal er tilgængelig for skabere i mere end 100 lande, hvorimod Stripe kun understøtter {n} land på en passende måde." +msgstr[1] "PayPal er tilgængelig for skabere i mere end 100 lande, hvorimod Stripe kun understøtter {n} lande på en passende måde." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/de.po b/i18n/core/de.po index 69320ba14..28935a81b 100644 --- a/i18n/core/de.po +++ b/i18n/core/de.po @@ -4184,9 +4184,9 @@ msgstr "Paypal unterstützt nur {n_paypal_currencies} von {n_liberapay_currencie #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "Paypal ist für Erstellende in mehr als 200 Ländern verfügbar, während Stripe nur {n} Länder adäquat unterstützt." +msgstr[1] "Paypal ist für Erstellende in mehr als 100 Ländern verfügbar, während Stripe nur {n} Länder adäquat unterstützt." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/el.po b/i18n/core/el.po index 9f158a4b8..c7e6995d2 100644 --- a/i18n/core/el.po +++ b/i18n/core/el.po @@ -4183,9 +4183,9 @@ msgstr "Το PayPal υποστηρίζει μόνο το {n_paypal_currencies} #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "Το PayPal είναι διαθέσιμο σε δημιουργούς σε περισσότερες από 200 χώρες, ενώ το Stripe υποστηρίζει μόνο τις χώρες {n} με κατάλληλο τρόπο." +msgstr[1] "Το PayPal είναι διαθέσιμο σε δημιουργούς σε περισσότερες από 100 χώρες, ενώ το Stripe υποστηρίζει μόνο τις χώρες {n} με κατάλληλο τρόπο." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/eo.po b/i18n/core/eo.po index 807cd5ddb..cb627c90b 100644 --- a/i18n/core/eo.po +++ b/i18n/core/eo.po @@ -4183,9 +4183,9 @@ msgstr "PayPal nur subtenas {n_paypal_currencies} el la {n_liberapay_currencies} #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal estas disponebla al kreantoj en pli ol 200 landoj, dum Stripe nur taŭge subtenas {n} landojn." +msgstr[1] "PayPal estas disponebla al kreantoj en pli ol 100 landoj, dum Stripe nur taŭge subtenas {n} landojn." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/es.po b/i18n/core/es.po index be6cb898e..81e5d2313 100644 --- a/i18n/core/es.po +++ b/i18n/core/es.po @@ -4184,9 +4184,9 @@ msgstr "PayPal solo soporta {n_paypal_currencies} de las {n_liberapay_currencies #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal está disponible para los creadores de más de 200 países, mientras que Stripe sólo admite {n} países de forma adecuada." +msgstr[1] "PayPal está disponible para los creadores de más de 100 países, mientras que Stripe sólo admite {n} países de forma adecuada." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/et.po b/i18n/core/et.po index e5d9bb9f2..089782646 100644 --- a/i18n/core/et.po +++ b/i18n/core/et.po @@ -4864,8 +4864,8 @@ msgstr "PayPal toetab ainult {n_paypal_currencies} {n_liberapay_currencies} valu #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal on kättesaadav enam kui 200 riigi loojatele, samas kui Stripe toetab sobival viisil ainult {n} riike." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal on kättesaadav enam kui 100 riigi loojatele, samas kui Stripe toetab sobival viisil ainult {n} riike." msgstr[1] "" #, fuzzy, python-brace-format diff --git a/i18n/core/fi.po b/i18n/core/fi.po index 5359599c0..a83d2a72b 100644 --- a/i18n/core/fi.po +++ b/i18n/core/fi.po @@ -4184,9 +4184,9 @@ msgstr "Liberapayn ja Stripen tukemista {n_liberapay_currencies} valuutasta vain #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "mörgö" -msgstr[1] "PayPal on sisällöntuottajien saatavilla yli 200 maassa, mutta Stripe tukee vain {n} maata soveltuvalla tavalla." +msgstr[1] "PayPal on sisällöntuottajien saatavilla yli 100 maassa, mutta Stripe tukee vain {n} maata soveltuvalla tavalla." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/fr.po b/i18n/core/fr.po index b143d5652..f63a98030 100644 --- a/i18n/core/fr.po +++ b/i18n/core/fr.po @@ -4184,9 +4184,9 @@ msgstr "PayPal ne prend en charge que {n_paypal_currencies} des {n_liberapay_cur #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal est disponible pour les créateurs dans plus de 200 pays, alors que Stripe ne prend en charge de manière appropriée que {n} pays." +msgstr[1] "PayPal est disponible pour les créateurs dans plus de 100 pays, alors que Stripe ne prend en charge de manière appropriée que {n} pays." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/fy.po b/i18n/core/fy.po index fc1b2b80e..b2b714580 100644 --- a/i18n/core/fy.po +++ b/i18n/core/fy.po @@ -4879,8 +4879,8 @@ msgstr "PayPal stipet allinich {n_paypal_currencies} fan 'e {n_liberapay_currenc #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal is beskikber foar makkers yn mear dan 200 lannen, wylst Stripe allinich {n} lannen op in gaadlike manier stipet." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal is beskikber foar makkers yn mear dan 100 lannen, wylst Stripe allinich {n} lannen op in gaadlike manier stipet." msgstr[1] "" #, fuzzy, python-brace-format diff --git a/i18n/core/ga.po b/i18n/core/ga.po index 4e2987e54..87786ce3e 100644 --- a/i18n/core/ga.po +++ b/i18n/core/ga.po @@ -4951,8 +4951,8 @@ msgstr "Ní thacaíonn PayPal ach le {n_paypal_currencies} de na hairgeadraí {n #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "Tá PayPal ar fáil do chruthaitheoirí i níos mó ná 200 tír, ach ní thacaíonn Stripe ach le {n} tíortha ar bhealach oiriúnach." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "Tá PayPal ar fáil do chruthaitheoirí i níos mó ná 100 tír, ach ní thacaíonn Stripe ach le {n} tíortha ar bhealach oiriúnach." msgstr[1] "" msgstr[2] "" diff --git a/i18n/core/hu.po b/i18n/core/hu.po index cfbc5b868..b6fd82c62 100644 --- a/i18n/core/hu.po +++ b/i18n/core/hu.po @@ -4353,8 +4353,8 @@ msgstr "A PayPal csak a {n_paypal_currencies} a {n_liberapay_currencies} pénzne #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "A PayPal több mint 200 országban áll az alkotók rendelkezésére, míg a Stripe csak a {n} országokat támogatja megfelelő módon." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "A PayPal több mint 100 országban áll az alkotók rendelkezésére, míg a Stripe csak a {n} országokat támogatja megfelelő módon." #, fuzzy, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/id.po b/i18n/core/id.po index 6fd2ad2f9..d19d7ea5a 100644 --- a/i18n/core/id.po +++ b/i18n/core/id.po @@ -4160,8 +4160,8 @@ msgstr "PayPal hanya mendukung {n_paypal_currencies} dari {n_liberapay_currencie #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal tersedia bagi para pembuat konten di lebih dari 200 negara, sedangkan Stripe hanya mendukung {n} negara dengan cara yang sesuai." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal tersedia bagi para pembuat konten di lebih dari 100 negara, sedangkan Stripe hanya mendukung {n} negara dengan cara yang sesuai." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/it.po b/i18n/core/it.po index e7424db8a..fb0d19b82 100644 --- a/i18n/core/it.po +++ b/i18n/core/it.po @@ -4184,9 +4184,9 @@ msgstr "PayPal supporta solo {n_paypal_currencies} delle {n_liberapay_currencies #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal è disponibile per i creatori in più di 200 Paesi, mentre Stripe supporta in modo adeguato solo {n} Paesi." +msgstr[1] "PayPal è disponibile per i creatori in più di 100 Paesi, mentre Stripe supporta in modo adeguato solo {n} Paesi." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/ja.po b/i18n/core/ja.po index 5bb70323d..381784fb9 100644 --- a/i18n/core/ja.po +++ b/i18n/core/ja.po @@ -4139,8 +4139,8 @@ msgstr "PayPalは、LiberapayとStripeがサポートする{n_liberapay_currenci #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPalは200カ国以上のクリエイターが利用できるのに対し、Stripeは{n}カ国しか適切に対応していません。" +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPalは100カ国以上のクリエイターが利用できるのに対し、Stripeは{n}カ国しか適切に対応していません。" #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/ko.po b/i18n/core/ko.po index 1793536b2..61f31cf35 100644 --- a/i18n/core/ko.po +++ b/i18n/core/ko.po @@ -4241,8 +4241,8 @@ msgstr "PayPal은 Liberapay 및 Stripe에서 지원하는 {n_liberapay_currencie #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal은 200개 이상의 국가에서 제작자가 사용할 수 있는 반면 Stripe는 {n} 국가만 적절한 방식으로 지원합니다." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal은 100개 이상의 국가에서 제작자가 사용할 수 있는 반면 Stripe는 {n} 국가만 적절한 방식으로 지원합니다." #, fuzzy, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/lt.po b/i18n/core/lt.po index 8ffa3a736..c102ee66b 100644 --- a/i18n/core/lt.po +++ b/i18n/core/lt.po @@ -4760,8 +4760,8 @@ msgstr "\"PayPal\" palaiko tik {n_paypal_currencies} iš {n_liberapay_currencies #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "\"PayPal\" gali naudotis daugiau nei 200 šalių kūrėjai, o \"Stripe\" tinkamu būdu palaiko tik {n} šalis." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "\"PayPal\" gali naudotis daugiau nei 100 šalių kūrėjai, o \"Stripe\" tinkamu būdu palaiko tik {n} šalis." msgstr[1] "" msgstr[2] "" diff --git a/i18n/core/lv.po b/i18n/core/lv.po index 25e70dff9..d67e14bea 100644 --- a/i18n/core/lv.po +++ b/i18n/core/lv.po @@ -4984,8 +4984,8 @@ msgstr "PayPal atbalsta tikai {n_paypal_currencies} no {n_liberapay_currencies} #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal ir pieejams autoriem vairāk nekā 200 valstīs, savukārt Stripe atbalsta tikai {n} valstis piemērotā veidā." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal ir pieejams autoriem vairāk nekā 100 valstīs, savukārt Stripe atbalsta tikai {n} valstis piemērotā veidā." msgstr[1] "" msgstr[2] "" diff --git a/i18n/core/ms.po b/i18n/core/ms.po index 9b8bf24d4..2eec64258 100644 --- a/i18n/core/ms.po +++ b/i18n/core/ms.po @@ -4184,9 +4184,9 @@ msgstr "PayPal hanya menyokong {n_paypal_currencies} daripada {n_liberapay_curre #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal tersedia kepada pencipta di lebih daripada 200 negara, manakala Stripe hanya menyokong {n} negara dalam cara yang sesuai." +msgstr[1] "PayPal tersedia kepada pencipta di lebih daripada 100 negara, manakala Stripe hanya menyokong {n} negara dalam cara yang sesuai." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/nb.po b/i18n/core/nb.po index 07a1dab0b..3900badc1 100644 --- a/i18n/core/nb.po +++ b/i18n/core/nb.po @@ -4255,8 +4255,8 @@ msgstr "PayPal støtter kun {n_paypal_currencies} av {n_liberapay_currencies}-va #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal er tilgjengelig for skapere i mer enn 200 land, mens Stripe kun støtter {n}-land på en passende måte." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal er tilgjengelig for skapere i mer enn 100 land, mens Stripe kun støtter {n}-land på en passende måte." msgstr[1] "" #, fuzzy, python-brace-format diff --git a/i18n/core/nl.po b/i18n/core/nl.po index 8f6643b61..aee469e99 100644 --- a/i18n/core/nl.po +++ b/i18n/core/nl.po @@ -4184,9 +4184,9 @@ msgstr "PayPal ondersteunt alleen {n_paypal_currencies} van de {n_liberapay_curr #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal is beschikbaar voor makers in meer dan 200 landen, terwijl Stripe slechts {n} landen op een geschikte manier ondersteunt." -msgstr[1] "PayPal is beschikbaar voor makers in meer dan 200 landen, terwijl Stripe slechts {n} landen op een geschikte manier ondersteunt." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal is beschikbaar voor makers in meer dan 100 landen, terwijl Stripe slechts {n} landen op een geschikte manier ondersteunt." +msgstr[1] "PayPal is beschikbaar voor makers in meer dan 100 landen, terwijl Stripe slechts {n} landen op een geschikte manier ondersteunt." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/pl.po b/i18n/core/pl.po index 790a88359..17cf60726 100644 --- a/i18n/core/pl.po +++ b/i18n/core/pl.po @@ -4228,10 +4228,10 @@ msgstr "PayPal obsługuje tylko {n_paypal_currencies} z {n_liberapay_currencies} #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal jest dostępny dla twórców w ponad 200 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} kraj." -msgstr[1] "PayPal jest dostępny dla twórców w ponad 200 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} kraje." -msgstr[2] "PayPal jest dostępny dla twórców w ponad 200 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} krajów." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal jest dostępny dla twórców w ponad 100 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} kraj." +msgstr[1] "PayPal jest dostępny dla twórców w ponad 100 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} kraje." +msgstr[2] "PayPal jest dostępny dla twórców w ponad 100 krajach, natomiast Stripe w odpowiedni sposób obsługuje tylko {n} krajów." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/pt.po b/i18n/core/pt.po index eedb251fb..ee611e3f4 100644 --- a/i18n/core/pt.po +++ b/i18n/core/pt.po @@ -4210,8 +4210,8 @@ msgstr "PayPal apenas suporta {n_paypal_currencies} das moedas {n_liberapay_curr #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal está disponível para criadores em mais de 200 países, enquanto que Stripe apenas apoia {n} países de uma forma adequada." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal está disponível para criadores em mais de 100 países, enquanto que Stripe apenas apoia {n} países de uma forma adequada." msgstr[1] "" #, fuzzy, python-brace-format diff --git a/i18n/core/ro.po b/i18n/core/ro.po index 5f157c481..2e04ebf72 100644 --- a/i18n/core/ro.po +++ b/i18n/core/ro.po @@ -4473,8 +4473,8 @@ msgstr "PayPal acceptă doar {n_paypal_currencies} din {n_liberapay_currencies} #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal este disponibil pentru creatorii din peste 200 de țări, în timp ce Stripe acceptă doar {n} țări într-un mod adecvat." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal este disponibil pentru creatorii din peste 100 de țări, în timp ce Stripe acceptă doar {n} țări într-un mod adecvat." msgstr[1] "" msgstr[2] "" diff --git a/i18n/core/ru.po b/i18n/core/ru.po index f38d9c638..e54703fa8 100644 --- a/i18n/core/ru.po +++ b/i18n/core/ru.po @@ -4229,10 +4229,10 @@ msgstr "PayPal поддерживает только {n_paypal_currencies} из #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal доступен для создателей в более чем 200 странах, в то время как Stripe поддерживает только {n} страну." -msgstr[1] "PayPal доступен для создателей в более чем 200 странах, в то время как Stripe поддерживает только {n} страны." -msgstr[2] "PayPal доступен для создателей в более чем 200 странах, в то время как Stripe поддерживает только {n} стран." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal доступен для создателей в более чем 100 странах, в то время как Stripe поддерживает только {n} страну." +msgstr[1] "PayPal доступен для создателей в более чем 100 странах, в то время как Stripe поддерживает только {n} страны." +msgstr[2] "PayPal доступен для создателей в более чем 100 странах, в то время как Stripe поддерживает только {n} стран." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/sk.po b/i18n/core/sk.po index b343da698..2cbe7d02f 100644 --- a/i18n/core/sk.po +++ b/i18n/core/sk.po @@ -4227,10 +4227,10 @@ msgstr "PayPal podporuje len {n_paypal_currencies} z {n_liberapay_currencies} mi #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal je dostupný tvorcom vo viac než 200 krajinách, kým Stripe podporuje len {n} krajiny vhodným spôsobom." -msgstr[2] "PayPal je dostupný tvorcom vo viac než 200 krajinách, kým Stripe podporuje len {n} krajín vhodným spôsobom." +msgstr[1] "PayPal je dostupný tvorcom vo viac než 100 krajinách, kým Stripe podporuje len {n} krajiny vhodným spôsobom." +msgstr[2] "PayPal je dostupný tvorcom vo viac než 100 krajinách, kým Stripe podporuje len {n} krajín vhodným spôsobom." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/sl.po b/i18n/core/sl.po index 97f05c5b3..7f9f5ff99 100644 --- a/i18n/core/sl.po +++ b/i18n/core/sl.po @@ -5028,8 +5028,8 @@ msgstr "PayPal podpira le {n_paypal_currencies} od {n_liberapay_currencies} valu #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal je ustvarjalcem na voljo v več kot 200 državah, Stripe pa na ustrezen način podpira le {n} držav." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal je ustvarjalcem na voljo v več kot 100 državah, Stripe pa na ustrezen način podpira le {n} držav." msgstr[1] "" msgstr[2] "" msgstr[3] "" diff --git a/i18n/core/sv.po b/i18n/core/sv.po index a93f10910..fd48a2eac 100644 --- a/i18n/core/sv.po +++ b/i18n/core/sv.po @@ -4199,9 +4199,9 @@ msgstr "PayPal stöder endast {n_paypal_currencies} av de {n_liberapay_currencie #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." msgstr[0] "" -msgstr[1] "PayPal är tillgängligt för skapare i mer än 200 länder, medan Stripe endast stöder {n} länder på ett lämpligt sätt." +msgstr[1] "PayPal är tillgängligt för skapare i mer än 100 länder, medan Stripe endast stöder {n} länder på ett lämpligt sätt." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/tr.po b/i18n/core/tr.po index 54c044423..ae7e0be12 100644 --- a/i18n/core/tr.po +++ b/i18n/core/tr.po @@ -4183,9 +4183,9 @@ msgstr "PayPal, Liberapay ve Stripe tarafından desteklenen {n_liberapay_currenc #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal 200'den fazla ülkedeki içerik oluşturucular tarafından kullanılabilirken, Stripe yalnızca {n} ülkeyi uygun bir şekilde desteklemektedir." -msgstr[1] "PayPal 200'den fazla ülkedeki içerik oluşturucular tarafından kullanılabilirken, Stripe yalnızca {n} ülkeyi uygun bir şekilde desteklemektedir." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal 100'den fazla ülkedeki içerik oluşturucular tarafından kullanılabilirken, Stripe yalnızca {n} ülkeyi uygun bir şekilde desteklemektedir." +msgstr[1] "PayPal 100'den fazla ülkedeki içerik oluşturucular tarafından kullanılabilirken, Stripe yalnızca {n} ülkeyi uygun bir şekilde desteklemektedir." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/uk.po b/i18n/core/uk.po index 1f26816da..bf6f7bf45 100644 --- a/i18n/core/uk.po +++ b/i18n/core/uk.po @@ -4228,10 +4228,10 @@ msgstr "PayPal підтримує лише {n_paypal_currencies} валют {n_l #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal доступний для творців у більш ніж 200 країнах, тоді як Stripe підтримує належним чином лише {n} країну." -msgstr[1] "PayPal доступний для творців у більш ніж 200 країнах, тоді як Stripe підтримує належним чином лише {n} країни." -msgstr[2] "PayPal доступний для творців у більш ніж 200 країнах, тоді як Stripe підтримує належним чином лише {n} країн." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal доступний для творців у більш ніж 100 країнах, тоді як Stripe підтримує належним чином лише {n} країну." +msgstr[1] "PayPal доступний для творців у більш ніж 100 країнах, тоді як Stripe підтримує належним чином лише {n} країни." +msgstr[2] "PayPal доступний для творців у більш ніж 100 країнах, тоді як Stripe підтримує належним чином лише {n} країн." #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/vi.po b/i18n/core/vi.po index bb3204cbe..0df649b62 100644 --- a/i18n/core/vi.po +++ b/i18n/core/vi.po @@ -4878,8 +4878,8 @@ msgstr "PayPal chỉ hỗ trợ {n_paypal_currencies} trong số các đơn vị #, fuzzy, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal có sẵn cho người sáng tạo ở hơn 200 quốc gia, trong khi Stripe chỉ hỗ trợ các quốc gia {n} theo cách phù hợp." +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal có sẵn cho người sáng tạo ở hơn 100 quốc gia, trong khi Stripe chỉ hỗ trợ các quốc gia {n} theo cách phù hợp." #, fuzzy, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/zh_Hans.po b/i18n/core/zh_Hans.po index 74a45bdec..6fca31bb1 100644 --- a/i18n/core/zh_Hans.po +++ b/i18n/core/zh_Hans.po @@ -4139,8 +4139,8 @@ msgstr "PayPal 只支持 {n_paypal_currencies} 种货币,Liberapay 和 Stripe #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal 可供 200 多个国家/地区的创作者使用,而 Stripe 仅以合适的方式支持 {n} 个国家。" +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal 可供 100 多个国家/地区的创作者使用,而 Stripe 仅以合适的方式支持 {n} 个国家。" #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/i18n/core/zh_Hant.po b/i18n/core/zh_Hant.po index 0024eaa28..995982a8a 100644 --- a/i18n/core/zh_Hant.po +++ b/i18n/core/zh_Hant.po @@ -4155,8 +4155,8 @@ msgstr "PayPal 只支持 {n_paypal_currencies} 種貨幣,Liberapay 和 Stripe #, python-brace-format msgid "" -msgid_plural "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way." -msgstr[0] "PayPal 可供 200 多個國家的創作者使用,而 Stripe 只以適當的方式支援 {n} 個國家。" +msgid_plural "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way." +msgstr[0] "PayPal 可供 100 多個國家的創作者使用,而 Stripe 只以適當的方式支援 {n} 個國家。" #, python-brace-format msgid "You can find more information on supported countries and currencies in {link_start}the “{page_name}” page{link_end}." diff --git a/www/about/global.spt b/www/about/global.spt index 2c0e0fc35..13449aa29 100644 --- a/www/about/global.spt +++ b/www/about/global.spt @@ -22,15 +22,27 @@ title = _("Global")

{{ _("Receive") }}

+

{{ _( + "Donations can only be received in territories where at least one supported " + "payment processor is available. The currently supported payment processors are " + "{Stripe} and {PayPal}. Some features are only available through Stripe, so " + "Liberapay is fully available to creators in territories supported by Stripe, and " + "partially available in territories only supported by PayPal.", + Stripe='Stripe'|safe, + PayPal='PayPal'|safe, +) }}

+ +% set stripe_countries = constants.PAYOUT_COUNTRIES['stripe'] +% set paypal_only_countries = constants.PAYOUT_COUNTRIES['paypal'] - stripe_countries

{{ ngettext( "", "Liberapay is fully available to creators in {n} territories:", - n=len(constants.PAYOUT_COUNTRIES['stripe']), + n=len(stripe_countries), ) }}

    % for country_code, localized_country_name in locale.countries.items() -% if country_code in constants.PAYOUT_COUNTRIES['stripe'] +% if country_code in stripe_countries
  • {{ localized_country_name }}
  • % endif % endfor @@ -38,11 +50,16 @@ title = _("Global")

    {{ ngettext( "", - "Additionally, Liberapay is partially available to creators in " - "{paypal_link_open}the {n} countries supported by PayPal{link_close}.", - n=len(constants.PAYOUT_COUNTRIES['paypal']), - paypal_link_open=''|safe, - link_close=''|safe + "Liberapay is partially available to creators in {n} territories:", + n=len(paypal_only_countries), ) }}

    +
      +% for country_code, localized_country_name in locale.countries.items() +% if country_code in paypal_only_countries +
    • {{ localized_country_name }}
    • +% endif +% endfor +
    + % endblock diff --git a/www/about/payment-processors.spt b/www/about/payment-processors.spt index 39ad829d2..9afaa9e15 100644 --- a/www/about/payment-processors.spt +++ b/www/about/payment-processors.spt @@ -29,7 +29,7 @@ title = _("Payment Processors") ) }}
  • {{ ngettext( "", - "PayPal is available to creators in more than 200 countries, whereas Stripe only supports {n} countries in a suitable way.", + "PayPal is available to creators in more than 100 countries, whereas Stripe only supports {n} countries in a suitable way.", n=len(constants.PAYOUT_COUNTRIES['stripe']), ) }}
From acfd2bab3c1ea4fe47e7a1240eef65c3e00a38cb Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 11 Sep 2023 18:25:13 +0200 Subject: [PATCH 07/13] fix incorrect year in Japanese translation --- i18n/core/ja.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/core/ja.po b/i18n/core/ja.po index 381784fb9..ee09b3eb9 100644 --- a/i18n/core/ja.po +++ b/i18n/core/ja.po @@ -4035,13 +4035,13 @@ msgstr "Liberapayの運営者" #, python-brace-format msgid "Liberapay is a non-profit organization {0}founded in 2015 in France{1} by {2} and {3}." -msgstr "Liberapayは{2}と{3}により、{0}2005年にフランスで設立された{1}非営利団体です。" +msgstr "Liberapayは{2}と{3}により、{0}2015年にフランスで設立された{1}非営利団体です。" msgid "Legal information" msgstr "法的情報" msgid "This website is managed by Liberapay, a non-profit organization legally founded in 2015 in France (identifier: W144000981)." -msgstr "このウェブサイトは、2005 年にフランスで法的に設立された非営利団体であるLiberapayにより管理されています(識別子:W144000981)。" +msgstr "このウェブサイトは、2015 年にフランスで法的に設立された非営利団体であるLiberapayにより管理されています(識別子:W144000981)。" msgid "Liberapay complies with the laws of the European Union. With the help of our partners we monitor transactions for possible fraud, money laundering, and terrorism financing." msgstr "Liberapayは EU(欧州連合)の法律に従っています。パートナーの皆様のご協力のもと、起こりうる詐欺・資金洗浄・テロへの資金供与を防ぐために取引を監視しています。" From dc128cb638fd07ea2f1a310599bd5a777f01e81c Mon Sep 17 00:00:00 2001 From: Changaco Date: Sat, 16 Sep 2023 10:51:13 +0200 Subject: [PATCH 08/13] fix inaccuracies in `upcoming_debit` notifications --- liberapay/payin/cron.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/liberapay/payin/cron.py b/liberapay/payin/cron.py index e27fb00d5..45400f9cd 100644 --- a/liberapay/payin/cron.py +++ b/liberapay/payin/cron.py @@ -211,6 +211,7 @@ def send_upcoming_debit_notifications(): 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 @@ -218,11 +219,12 @@ def send_upcoming_debit_notifications(): WHERE r.participant = %s AND r.status = 'chargeable' AND r.network::text LIKE 'stripe-%%' - ORDER BY r.is_default NULLS LAST + 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,)) + """, (payer.id, currency)) if route is None: break route.sync_status() From a7b61afa3563b357906020cd24fcc762b1af72ab Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 17 Sep 2023 10:26:19 +0200 Subject: [PATCH 09/13] clear auto-converted monetary constants every day --- liberapay/constants.py | 77 ++++++++++++++++++++++++++---------- liberapay/i18n/base.py | 5 ++- liberapay/i18n/currencies.py | 8 ++++ tests/py/test_cron.py | 10 +++-- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/liberapay/constants.py b/liberapay/constants.py index cd6fcddf8..a3ff95dfc 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -1,4 +1,4 @@ -from collections import defaultdict, namedtuple +from collections import namedtuple from datetime import date, datetime, timedelta from decimal import Decimal, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP import re @@ -48,19 +48,58 @@ def convert_symbolic_amount(amount, target_currency, precision=2, rounding=ROUND ) -class MoneyAutoConvertDict(defaultdict): +class MoneyAutoConvertDict(dict): + __slots__ = ('constants', 'precision') - def __init__(self, *args, precision=2): - super().__init__(None, *args) + instances = [] + # Note: our instances of this class aren't ephemeral, so a simple list is + # intentionally used here instead of weak references. + lock = Lock() + + def __init__(self, constant_items, precision=2): + super().__init__(constant_items) + self.constants = set(constant_items.keys()) self.precision = precision + self.instances.append(self) + + def __delitem__(self): + raise NotImplementedError() + + def __ior__(self): + raise NotImplementedError() def __missing__(self, currency): - r = Money( + with self.lock: + r = self.generate_value(currency) + dict.__setitem__(self, currency, r) + return r + + def __setitem__(self): + raise NotImplementedError() + + def clear(self): + """Clear all the auto-converted amounts. + """ + with self.lock: + for currency in list(self): + if currency not in self.constants: + dict.__delitem__(self, currency) + + def generate_value(self, currency): + return Money( convert_symbolic_amount(self['EUR'].amount, currency, self.precision), - currency + currency, + rounding=ROUND_UP, ) - self[currency] = r - return r + + def pop(self): + raise NotImplementedError() + + def popitem(self): + raise NotImplementedError() + + def update(self): + raise NotImplementedError() StandardTip = namedtuple('StandardTip', 'label weekly monthly yearly') @@ -94,15 +133,16 @@ def __missing__(self, currency): } -class _DonationLimits(defaultdict): - def __missing__(self, currency): +class _DonationLimits(MoneyAutoConvertDict): + + def generate_value(self, currency): minimum = Money.MINIMUMS[currency].amount eur_weekly_amounts = DONATION_LIMITS_EUR_USD['weekly'] converted_weekly_amounts = ( convert_symbolic_amount(eur_weekly_amounts[0], currency), convert_symbolic_amount(eur_weekly_amounts[1], currency) ) - r = { + return { 'weekly': tuple(Money(x, currency) for x in converted_weekly_amounts), 'monthly': tuple( Money((x * Decimal(52) / Decimal(12)).quantize(minimum, rounding=ROUND_UP), currency) @@ -110,8 +150,6 @@ def __missing__(self, currency): ), 'yearly': tuple(Money(x * Decimal(52), currency) for x in converted_weekly_amounts), } - self[currency] = r - return r DONATION_LIMITS_WEEKLY_EUR_USD = (Decimal('0.01'), Decimal('100.00')) DONATION_LIMITS_EUR_USD = { @@ -120,7 +158,7 @@ def __missing__(self, currency): for x in DONATION_LIMITS_WEEKLY_EUR_USD), 'yearly': tuple(x * Decimal(52) for x in DONATION_LIMITS_WEEKLY_EUR_USD), } -DONATION_LIMITS = _DonationLimits(None, { +DONATION_LIMITS = _DonationLimits({ 'EUR': {k: (Money(v[0], 'EUR'), Money(v[1], 'EUR')) for k, v in DONATION_LIMITS_EUR_USD.items()}, 'USD': {k: (Money(v[0], 'USD'), Money(v[1], 'USD')) for k, v in DONATION_LIMITS_EUR_USD.items()}, }) @@ -386,15 +424,14 @@ def make_standard_tip(label, weekly, currency): ) -class _StandardTips(defaultdict): - def __missing__(self, currency): - r = [ +class _StandardTips(MoneyAutoConvertDict): + + def generate_value(self, currency): + return [ make_standard_tip( label, convert_symbolic_amount(weekly, currency), currency ) for label, weekly in STANDARD_TIPS_EUR_USD ] - self[currency] = r - return r STANDARD_TIPS_EUR_USD = ( @@ -404,7 +441,7 @@ def __missing__(self, currency): (_("Large"), Decimal('5.00')), (_("Maximum"), DONATION_LIMITS_EUR_USD['weekly'][1]), ) -STANDARD_TIPS = _StandardTips(None, { +STANDARD_TIPS = _StandardTips({ 'EUR': [make_standard_tip(label, weekly, 'EUR') for label, weekly in STANDARD_TIPS_EUR_USD], 'USD': [make_standard_tip(label, weekly, 'USD') for label, weekly in STANDARD_TIPS_EUR_USD], }) diff --git a/liberapay/i18n/base.py b/liberapay/i18n/base.py index a2335e7bf..bdead0a89 100644 --- a/liberapay/i18n/base.py +++ b/liberapay/i18n/base.py @@ -14,10 +14,11 @@ import opencc from pando.utils import utcnow -from ..constants import to_precision from ..exceptions import AmbiguousNumber, InvalidNumber from ..website import website -from .currencies import CURRENCIES, CURRENCY_REPLACEMENTS, D_MAX, Money, MoneyBasket +from .currencies import ( + CURRENCIES, CURRENCY_REPLACEMENTS, D_MAX, Money, MoneyBasket, to_precision, +) MONEY_AMOUNT_FORMAT = parse_pattern('#,##0.00') diff --git a/liberapay/i18n/currencies.py b/liberapay/i18n/currencies.py index 8183ca38d..0b488d953 100644 --- a/liberapay/i18n/currencies.py +++ b/liberapay/i18n/currencies.py @@ -483,6 +483,14 @@ def fetch_currency_exchange_rates(db=None): ON CONFLICT (source_currency, target_currency) DO UPDATE SET rate = excluded.rate """, dict(target=currency, rate=Decimal(fx['@rate']))) + # Update the local cache, unless it hasn't been created yet. + if hasattr(website, 'currency_exchange_rates'): + website.currency_exchange_rates = get_currency_exchange_rates(db) + # Clear the cached auto-converted money amounts, so they'll be recomputed + # with the new exchange rates. + from ..constants import MoneyAutoConvertDict + for d in MoneyAutoConvertDict.instances: + d.clear() def get_currency_exchange_rates(db): diff --git a/tests/py/test_cron.py b/tests/py/test_cron.py index 11c477edc..f56dd30a1 100644 --- a/tests/py/test_cron.py +++ b/tests/py/test_cron.py @@ -110,9 +110,13 @@ def test_disabled_job_is_not_run(self): job.period = period def test_fetch_currency_exchange_rates(self): - with self.allow_changes_to('currency_exchange_rates'), self.db.get_cursor() as cursor: - fetch_currency_exchange_rates(cursor) - cursor.connection.rollback() + currency_exchange_rates = self.client.website.currency_exchange_rates.copy() + try: + with self.allow_changes_to('currency_exchange_rates'), self.db.get_cursor() as cursor: + fetch_currency_exchange_rates(cursor) + cursor.connection.rollback() + finally: + self.client.website.currency_exchange_rates = currency_exchange_rates def test_send_account_disabled_notifications(self): admin = self.make_participant('admin', privileges=1) From f35f59ace2143bc3a56c5f142b3fa3efdc31d13d Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 17 Sep 2023 11:44:49 +0200 Subject: [PATCH 10/13] move some code around --- liberapay/constants.py | 91 ++---------------------------------- liberapay/i18n/currencies.py | 91 +++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 89 deletions(-) diff --git a/liberapay/constants.py b/liberapay/constants.py index a3ff95dfc..960a67baa 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -1,12 +1,14 @@ from collections import namedtuple from datetime import date, datetime, timedelta -from decimal import Decimal, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP +from decimal import Decimal, ROUND_HALF_UP, ROUND_UP import re from markupsafe import Markup from pando.utils import utc -from .i18n.currencies import CURRENCIES, D_CENT, Money # noqa: F401 +from .i18n.currencies import ( # noqa: F401 + convert_symbolic_amount, CURRENCIES, D_CENT, Money, MoneyAutoConvertDict, +) def check_bits(bits): @@ -17,91 +19,6 @@ def check_bits(bits): Event = namedtuple('Event', 'name bit title') -def to_precision(x, precision, rounding=ROUND_HALF_UP): - """Round `x` to keep only `precision` of its most significant digits. - - >>> to_precision(Decimal('0.0086820'), 2) - Decimal('0.0087') - >>> to_precision(Decimal('13567.89'), 3) - Decimal('13600') - >>> to_precision(Decimal('0.000'), 4) - Decimal('0') - """ - if x == 0: - return Decimal(0) - log10 = x.log10().to_integral(ROUND_FLOOR) - # round - factor = Decimal(10) ** (log10 + 1) - r = (x / factor).quantize(Decimal(10) ** -precision, rounding=rounding) * factor - # remove trailing zeros - r = r.quantize(Decimal(10) ** (log10 - precision + 1)) - return r - - -def convert_symbolic_amount(amount, target_currency, precision=2, rounding=ROUND_HALF_UP): - from liberapay.website import website - rate = website.currency_exchange_rates[('EUR', target_currency)] - minimum = Money.MINIMUMS[target_currency].amount - return max( - to_precision(amount * rate, precision, rounding).quantize(minimum, rounding), - minimum - ) - - -class MoneyAutoConvertDict(dict): - __slots__ = ('constants', 'precision') - - instances = [] - # Note: our instances of this class aren't ephemeral, so a simple list is - # intentionally used here instead of weak references. - lock = Lock() - - def __init__(self, constant_items, precision=2): - super().__init__(constant_items) - self.constants = set(constant_items.keys()) - self.precision = precision - self.instances.append(self) - - def __delitem__(self): - raise NotImplementedError() - - def __ior__(self): - raise NotImplementedError() - - def __missing__(self, currency): - with self.lock: - r = self.generate_value(currency) - dict.__setitem__(self, currency, r) - return r - - def __setitem__(self): - raise NotImplementedError() - - def clear(self): - """Clear all the auto-converted amounts. - """ - with self.lock: - for currency in list(self): - if currency not in self.constants: - dict.__delitem__(self, currency) - - def generate_value(self, currency): - return Money( - convert_symbolic_amount(self['EUR'].amount, currency, self.precision), - currency, - rounding=ROUND_UP, - ) - - def pop(self): - raise NotImplementedError() - - def popitem(self): - raise NotImplementedError() - - def update(self): - raise NotImplementedError() - - StandardTip = namedtuple('StandardTip', 'label weekly monthly yearly') diff --git a/liberapay/i18n/currencies.py b/liberapay/i18n/currencies.py index 0b488d953..5f519b804 100644 --- a/liberapay/i18n/currencies.py +++ b/liberapay/i18n/currencies.py @@ -1,8 +1,11 @@ from datetime import datetime -from decimal import Decimal, InvalidOperation, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP +from decimal import ( + Decimal, InvalidOperation, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP, +) from itertools import chain, starmap, zip_longest from numbers import Number import operator +from threading import Lock from pando.utils import utc import requests @@ -466,6 +469,90 @@ def fuzzy_sum(self, currency, rounding=ROUND_UP): return Money(a, currency, rounding=rounding, fuzzy=fuzzy) +def to_precision(x, precision, rounding=ROUND_HALF_UP): + """Round `x` to keep only `precision` of its most significant digits. + + >>> to_precision(Decimal('0.0086820'), 2) + Decimal('0.0087') + >>> to_precision(Decimal('13567.89'), 3) + Decimal('13600') + >>> to_precision(Decimal('0.000'), 4) + Decimal('0') + """ + if x == 0: + return Decimal(0) + log10 = x.log10().to_integral(ROUND_FLOOR) + # round + factor = Decimal(10) ** (log10 + 1) + r = (x / factor).quantize(Decimal(10) ** -precision, rounding=rounding) * factor + # remove trailing zeros + r = r.quantize(Decimal(10) ** (log10 - precision + 1)) + return r + + +def convert_symbolic_amount(amount, target_currency, precision=2, rounding=ROUND_HALF_UP): + rate = website.currency_exchange_rates[('EUR', target_currency)] + minimum = Money.MINIMUMS[target_currency].amount + return max( + to_precision(amount * rate, precision, rounding).quantize(minimum, rounding), + minimum + ) + + +class MoneyAutoConvertDict(dict): + __slots__ = ('constants', 'precision') + + instances = [] + # Note: our instances of this class aren't ephemeral, so a simple list is + # intentionally used here instead of weak references. + lock = Lock() + + def __init__(self, constant_items, precision=2): + super().__init__(constant_items) + self.constants = set(constant_items.keys()) + self.precision = precision + self.instances.append(self) + + def __delitem__(self): + raise NotImplementedError() + + def __ior__(self): + raise NotImplementedError() + + def __missing__(self, currency): + with self.lock: + r = self.generate_value(currency) + dict.__setitem__(self, currency, r) + return r + + def __setitem__(self): + raise NotImplementedError() + + def clear(self): + """Clear all the auto-converted amounts. + """ + with self.lock: + for currency in list(self): + if currency not in self.constants: + dict.__delitem__(self, currency) + + def generate_value(self, currency): + return Money( + convert_symbolic_amount(self['EUR'].amount, currency, self.precision), + currency, + rounding=ROUND_UP, + ) + + def pop(self): + raise NotImplementedError() + + def popitem(self): + raise NotImplementedError() + + def update(self): + raise NotImplementedError() + + def fetch_currency_exchange_rates(db=None): db = db or website.db currencies = set(db.one("SELECT array_to_json(enum_range(NULL::currency))")) @@ -488,7 +575,6 @@ def fetch_currency_exchange_rates(db=None): website.currency_exchange_rates = get_currency_exchange_rates(db) # Clear the cached auto-converted money amounts, so they'll be recomputed # with the new exchange rates. - from ..constants import MoneyAutoConvertDict for d in MoneyAutoConvertDict.instances: d.clear() @@ -503,3 +589,4 @@ def get_currency_exchange_rates(db): for currency in CURRENCY_REPLACEMENTS: del CURRENCIES[currency] + del currency From 5dafe632e85cf57dfbabc56e06229bf0bde98396 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 17 Sep 2023 14:51:47 +0200 Subject: [PATCH 11/13] expand some tests --- tests/py/test_cron.py | 4 ++++ tests/py/test_schedule.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/py/test_cron.py b/tests/py/test_cron.py index f56dd30a1..7706f5bbe 100644 --- a/tests/py/test_cron.py +++ b/tests/py/test_cron.py @@ -4,6 +4,7 @@ from pando.utils import utcnow +from liberapay.constants import PAYIN_AMOUNTS from liberapay.cron import Daily, Weekly from liberapay.i18n.currencies import fetch_currency_exchange_rates from liberapay.models.participant import ( @@ -110,6 +111,8 @@ def test_disabled_job_is_not_run(self): job.period = period def test_fetch_currency_exchange_rates(self): + assert PAYIN_AMOUNTS['paypal']['min_acceptable']['HUF'] + assert 'HUF' in PAYIN_AMOUNTS['paypal']['min_acceptable'] currency_exchange_rates = self.client.website.currency_exchange_rates.copy() try: with self.allow_changes_to('currency_exchange_rates'), self.db.get_cursor() as cursor: @@ -117,6 +120,7 @@ def test_fetch_currency_exchange_rates(self): cursor.connection.rollback() finally: self.client.website.currency_exchange_rates = currency_exchange_rates + assert 'HUF' not in PAYIN_AMOUNTS['paypal']['min_acceptable'] def test_send_account_disabled_notifications(self): admin = self.make_participant('admin', privileges=1) diff --git a/tests/py/test_schedule.py b/tests/py/test_schedule.py index 8601587ee..575dd0881 100644 --- a/tests/py/test_schedule.py +++ b/tests/py/test_schedule.py @@ -5,6 +5,7 @@ from liberapay.billing.payday import compute_next_payday_date from liberapay.i18n.base import LOCALE_EN +from liberapay.models.participant import Participant from liberapay.payin.common import update_payin_transfer from liberapay.payin.cron import ( execute_scheduled_payins, @@ -742,6 +743,17 @@ def test_late_manual_payment_switched_to_automatic_is_scheduled_a_week_away(self class TestScheduledPayins(EmailHarness): + def setUp(self): + super().setUp() + schedule_renewals = Participant.schedule_renewals + self.sr_patch = patch.object(Participant, 'schedule_renewals', autospec=True) + self.sr_mock = self.sr_patch.__enter__() + self.sr_mock.side_effect = schedule_renewals + + def tearDown(self): + self.sr_patch.__exit__(None, None, None) + super().tearDown() + def test_no_scheduled_payins(self): self.make_participant('alice') send_upcoming_debit_notifications() @@ -783,6 +795,7 @@ def test_one_scheduled_payin(self): SET execution_date = current_date , last_notif_ts = (last_notif_ts - interval '14 days') """) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 2 @@ -798,6 +811,7 @@ def test_one_scheduled_payin(self): assert len(emails) == 1 assert emails[0]['to'] == ['alice '] assert emails[0]['subject'] == "Your payment has succeeded" + assert self.sr_mock.call_count == 1 scheduled_payins = self.db.all( "SELECT * FROM scheduled_payins ORDER BY execution_date" @@ -859,6 +873,7 @@ def test_multiple_scheduled_payins(self): SET execution_date = %(execution_date)s WHERE id = %(id)s """, manual_payin.__dict__) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 4 @@ -882,6 +897,7 @@ def test_multiple_scheduled_payins(self): assert len(emails) == 1 assert emails[0]['to'] == ['alice '] assert emails[0]['subject'] == "Your payment has succeeded" + assert self.sr_mock.call_count == 1 def test_early_manual_renewal_of_automatic_donations(self): alice = self.make_participant('alice', email='alice@liberapay.com') @@ -969,6 +985,7 @@ def test_canceled_and_impossible_transfers(self): , last_notif_ts = (last_notif_ts - interval '14 days') , ctime = (ctime - interval '12 hours') """) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 3 @@ -978,6 +995,7 @@ def test_canceled_and_impossible_transfers(self): assert emails[0]['subject'] == 'Liberapay donation renewal: payment aborted' assert emails[1]['to'] == ['alice '] assert emails[1]['subject'] == 'Liberapay donation renewal: payment aborted' + assert self.sr_mock.call_count == 0 def test_canceled_scheduled_payin(self): alice = self.make_participant('alice', email='alice@liberapay.com') @@ -1036,6 +1054,7 @@ def test_scheduled_payin_suspended_payer(self): , last_notif_ts = (last_notif_ts - interval '14 days') , ctime = (ctime - interval '12 hours') """) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 1 @@ -1044,6 +1063,7 @@ def test_scheduled_payin_suspended_payer(self): assert payins[0].off_session is False emails = self.get_emails() assert len(emails) == 0 + assert self.sr_mock.call_count == 0 def test_missing_route(self): alice = self.make_participant('alice', email='alice@liberapay.com') @@ -1074,6 +1094,7 @@ def test_missing_route(self): SET execution_date = current_date , last_notif_ts = (last_notif_ts - interval '14 days') """) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 1 @@ -1082,6 +1103,7 @@ def test_missing_route(self): assert payins[0].off_session is False emails = self.get_emails() assert len(emails) == 0 + assert self.sr_mock.call_count == 0 def test_scheduled_payin_requiring_authentication(self): alice = self.make_participant('alice', email='alice@liberapay.com') @@ -1114,6 +1136,7 @@ def test_scheduled_payin_requiring_authentication(self): , last_notif_ts = (last_notif_ts - interval '14 days') , ctime = (ctime - interval '12 hours') """) + self.sr_mock.reset_mock() execute_scheduled_payins() payins = self.db.all("SELECT * FROM payins ORDER BY ctime") assert len(payins) == 2 @@ -1130,6 +1153,7 @@ def test_scheduled_payin_requiring_authentication(self): scheduled_payins = self.db.all("SELECT * FROM scheduled_payins ORDER BY id") assert len(scheduled_payins) == 1 assert scheduled_payins[0].payin == payins[1].id + assert self.sr_mock.call_count == 0 # Test the payin page, it should redirect to the 3DSecure page r = self.client.GET(payin_page_path, auth_as=alice, raise_immediately=False) assert r.code == 200 @@ -1168,9 +1192,11 @@ def test_scheduled_automatic_payin_currency_unaccepted_before_reminder(self): , last_notif_ts = (last_notif_ts - interval '14 days') , ctime = (ctime - interval '12 hours') """) + self.sr_mock.reset_mock() execute_scheduled_payins() emails = self.get_emails() assert len(emails) == 0 + assert self.sr_mock.call_count == 0 send_donation_reminder_notifications() emails = self.get_emails() assert len(emails) == 1 @@ -1220,6 +1246,7 @@ def test_scheduled_automatic_payin_currency_unaccepted_after_reminder(self): , last_notif_ts = (last_notif_ts - interval '14 days') , ctime = (ctime - interval '12 hours') """) + self.sr_mock.reset_mock() execute_scheduled_payins() emails = self.get_emails() assert len(emails) == 1 @@ -1229,6 +1256,7 @@ def test_scheduled_automatic_payin_currency_unaccepted_after_reminder(self): assert len(scheduled_payins) == 1 assert scheduled_payins[0].payin is None assert scheduled_payins[0].notifs_count == 2 + assert self.sr_mock.call_count == 0 def test_cancelling_a_scheduled_payin(self): alice = self.make_participant('alice', email='alice@liberapay.com') From d3b16e139565f3ff87d5edef51d56cd2bd242cf0 Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 18 Sep 2023 09:31:34 +0200 Subject: [PATCH 12/13] add more constraints on profile descriptions --- tests/py/test_profile_edit.py | 33 ++++++++++++------ www/%username/edit/statement.spt | 59 ++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/tests/py/test_profile_edit.py b/tests/py/test_profile_edit.py index c69bc64cf..1d4739c39 100644 --- a/tests/py/test_profile_edit.py +++ b/tests/py/test_profile_edit.py @@ -1,37 +1,48 @@ from liberapay.testing import Harness +LOREM_IPSUM = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore " + "eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt " + "in culpa qui officia deserunt mollit anim id est laborum." +) + + class Tests(Harness): - def edit_statement(self, lang, statement, auth_as='alice', action='publish'): + def edit_statement(self, lang, statement, auth_as='alice', action='publish', summary=''): alice = self.make_participant('alice') return self.client.POST( "/alice/edit/statement", - {'lang': lang, 'statement': statement, 'action': action}, + {'lang': lang, 'statement': statement, 'summary': summary, 'action': action}, auth_as=alice if auth_as == 'alice' else auth_as, raise_immediately=False ) def test_anonymous_gets_403(self): - r = self.edit_statement('en', 'Some statement', auth_as=None) + r = self.edit_statement('en', LOREM_IPSUM, auth_as=None) assert r.code == 403 def test_participant_can_change_their_statement(self): - r = self.edit_statement('en', 'Lorem ipsum') + r = self.edit_statement('en', LOREM_IPSUM) assert r.code == 302 r = self.client.GET('/alice/') - assert '

Lorem ipsum

' in r.text, r.text + assert LOREM_IPSUM in r.text, r.text def test_participant_can_preview_their_statement(self): - r = self.edit_statement('en', 'Lorem ipsum', action='preview') + r = self.edit_statement('en', LOREM_IPSUM, action='preview') assert r.code == 200 - assert '

Lorem ipsum

' in r.text, r.text + assert LOREM_IPSUM in r.text, r.text def test_participant_can_switch_language(self): alice = self.make_participant('alice') r = self.client.PxST( "/alice/edit/statement", - {'lang': 'en', 'switch_lang': 'fr', 'statement': '', 'action': 'switch'}, + {'lang': 'en', 'switch_lang': 'fr', 'statement': '', 'summary': '', + 'action': 'switch'}, auth_as=alice ) assert r.code == 302 @@ -41,14 +52,16 @@ def test_participant_is_warned_of_unsaved_changes_when_switching_language(self): alice = self.make_participant('alice') r = self.client.POST( "/alice/edit/statement", - {'lang': 'en', 'switch_lang': 'fr', 'statement': 'foo', 'action': 'switch'}, + {'lang': 'en', 'switch_lang': 'fr', 'statement': 'foo', 'summary': '', + 'action': 'switch'}, auth_as=alice ) assert r.code == 200 assert " are you sure you want to discard them?" in r.text, r.text r = self.client.PxST( "/alice/edit/statement", - {'lang': 'en', 'switch_lang': 'fr', 'statement': 'foo', 'action': 'switch', 'discard': 'yes'}, + {'lang': 'en', 'switch_lang': 'fr', 'statement': 'foo', 'summary': '', + 'action': 'switch', 'discard': 'yes'}, auth_as=alice ) assert r.code == 302 diff --git a/www/%username/edit/statement.spt b/www/%username/edit/statement.spt index 58e3c3193..23aa9f789 100644 --- a/www/%username/edit/statement.spt +++ b/www/%username/edit/statement.spt @@ -4,32 +4,47 @@ from liberapay.utils import excerpt_intro, form_post_success, get_participant, m [---] participant = get_participant(state, restrict=True, allow_member=True) +errors = [] + if request.method == 'POST': lang = request.body['lang'] switch_lang = request.body.get('switch_lang') - summary = request.body.get('summary') or '' - statement = request.body['statement'] - - action = request.body.get('action') - if action is None: - # Temporary fallback - if request.body.get('save') == 'true': - action = 'publish' - elif request.body.get('preview') == 'true': - action = 'preview' - else: - action = 'edit' + summary = request.body['summary'].strip() + statement = request.body['statement'].strip() + action = request.body.get_choice('action', ('edit', 'preview', 'publish', 'switch')) if lang not in ACCEPTED_LANGUAGES: raise response.invalid_input(lang, 'lang', 'body') if switch_lang and switch_lang not in ACCEPTED_LANGUAGES: raise response.invalid_input(lang, 'switch_lang', 'body') - if len(summary) > constants.SUMMARY_MAX_SIZE: - raise response.error(400, _( - "The submitted summary is too long ({0} > {1}).", - len(summary), constants.SUMMARY_MAX_SIZE) - ) + if action != 'switch': + if len(summary) > constants.SUMMARY_MAX_SIZE: + errors.append(ngettext( + "", + "The summary can't be more than {n} characters long.", + constants.SUMMARY_MAX_SIZE + )) + if len(statement) < 100: + errors.append(ngettext( + "", + "The full description must be at least {n} characters long.", + n=100 + )) + elif len(statement) > 50_000: + errors.append(ngettext( + "", + "The full description can't be more than {n} characters long.", + n=50_000 + )) + if statement and statement == summary: + errors.append(_("The full description can't be identical to the summary.")) + if summary in (participant.username, participant.public_name): + errors.append(_("The summary can't be only your name.")) + if statement in (participant.username, participant.public_name): + errors.append(_("The description can't be only your name.")) + if errors: + action = 'edit' if action == 'publish': participant.upsert_statement(lang, summary, 'summary') @@ -83,8 +98,9 @@ subhead = _("Descriptions")
-

{{ _("Statement") }}

+ +

{{ _("Statement") }}

{{ rendered_stmt }}

{{ _("Excerpt that will be used in social media:") }}

@@ -92,7 +108,6 @@ subhead = _("Descriptions")
% if summary

{{ _("Preview of the short description") }}

- {{ profile_box_embedded(participant, summary) }}
% if participant.username in summary @@ -120,9 +135,7 @@ subhead = _("Descriptions") - % if summary - % endif
@@ -133,6 +146,10 @@ subhead = _("Descriptions") % else + % for error in errors +

{{ error }}

+ % endfor +

{{ _( "Describe your work, why you're asking for donations, etc. The short " "summary will be used when showcasing your profile alongside others, " From 2550134394acb1f313c41f902b3e9b211e5e79ab Mon Sep 17 00:00:00 2001 From: Changaco Date: Mon, 18 Sep 2023 11:45:28 +0200 Subject: [PATCH 13/13] replace the remaining uses of the word "statement" --- www/%username/edit/statement.spt | 2 +- www/%username/index.html.spt | 2 +- www/search.spt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/%username/edit/statement.spt b/www/%username/edit/statement.spt index 23aa9f789..589d2f1eb 100644 --- a/www/%username/edit/statement.spt +++ b/www/%username/edit/statement.spt @@ -100,7 +100,7 @@ subhead = _("Descriptions") -

{{ _("Statement") }}

+

{{ _("Description") }}

{{ rendered_stmt }}

{{ _("Excerpt that will be used in social media:") }}

diff --git a/www/%username/index.html.spt b/www/%username/index.html.spt index 04f49a5fc..653a55fdd 100644 --- a/www/%username/index.html.spt +++ b/www/%username/index.html.spt @@ -158,7 +158,7 @@ show_income = not participant.hide_receiving and participant.accepts_tips % block content % if statement -

{{ _("Statement") }}

+

{{ _("Description") }}

{{ statement_html }}
diff --git a/www/search.spt b/www/search.spt index 4c8c48652..a5b174962 100644 --- a/www/search.spt +++ b/www/search.spt @@ -172,8 +172,8 @@ if query: % if statements
-

{{ ngettext("Found a matching user statement", - "Found matching user statements", +

{{ ngettext("Found a matching user description", + "Found matching user descriptions", len(statements)) }}

% for result in statements