Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Enable creators to choose which countries donations are allowed to come from #2302

Merged
merged 2 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions liberapay/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,31 @@ def msg(self, _):
)


class ProhibitedSourceCountry(LazyResponseXXX):
code = 403

def __init__(self, recipient, country):
super().__init__()
self.recipient = recipient
self.country = country

def msg(self, _, locale):
return _(
"{username} does not accept donations from {country}.",
username=self.recipient.username, country=locale.Country(self.country)
)


class UnableToDeterminePayerCountry(LazyResponseXXX):
code = 500
def msg(self, _):
return _(
"The processing of your payment has failed because our software was "
"unable to determine which country the money would come from. This "
"isn't supposed to happen."
)


class TooManyCurrencyChanges(LazyResponseXXX):
code = 429
def msg(self, _):
Expand Down
2 changes: 1 addition & 1 deletion liberapay/i18n/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,8 @@ def make_sorted_dict(keys, d, d2={}, clean=_return_):
SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR
TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW
""".split()

COUNTRIES = make_sorted_dict(COUNTRY_CODES, LOCALE_EN.territories)
del COUNTRY_CODES


def make_currencies_map():
Expand Down
11 changes: 9 additions & 2 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,14 +1740,21 @@ def send_newsletters(cls):

@cached_property
def recipient_settings(self):
return self.db.one("""
r = self.db.one("""
SELECT *
FROM recipient_settings
WHERE participant = %s
""", (self.id,), default=Object(
participant=self.id,
patron_visibilities=(7 if self.status == 'stub' else 0),
patron_visibilities=(7 if self.status == 'stub' else None),
patron_countries=None,
))
if r.patron_countries:
if r.patron_countries.startswith('-'):
r.patron_countries = set(i18n.COUNTRIES) - set(r.patron_countries[1:].split(','))
else:
r.patron_countries = set(r.patron_countries.split(','))
return r

def update_recipient_settings(self, **kw):
cols, vals = zip(*kw.items())
Expand Down
16 changes: 15 additions & 1 deletion liberapay/payin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from ..constants import SEPA
from ..exceptions import (
AccountSuspended, BadDonationCurrency, MissingPaymentAccount,
RecipientAccountSuspended, NoSelfTipping, UserDoesntAcceptTips,
NoSelfTipping, ProhibitedSourceCountry, RecipientAccountSuspended,
UnableToDeterminePayerCountry, UserDoesntAcceptTips,
)
from ..i18n.currencies import Money, MoneyBasket
from ..utils import group_by
Expand Down Expand Up @@ -58,6 +59,19 @@ def prepare_payin(db, payer, amount, route, proto_transfers, off_session=False):
if payer.is_suspended or not payer.get_email_address():
raise AccountSuspended()

if route.network == 'paypal':
# The country of origin check for PayPal payments is in the
# `liberapay.payin.paypal.capture_order` function.
pass
else:
for pt in proto_transfers:
if (allowed_countries := pt.recipient.recipient_settings.patron_countries):
if route.country not in allowed_countries:
if route.country:
raise ProhibitedSourceCountry(pt.recipient, route.country)
else:
raise UnableToDeterminePayerCountry()

with db.get_cursor() as cursor:
payin = cursor.one("""
INSERT INTO payins
Expand Down
33 changes: 32 additions & 1 deletion liberapay/payin/paypal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import requests
from pando.utils import utcnow

from ..exceptions import PaymentError
from ..exceptions import (
PaymentError, ProhibitedSourceCountry, UnableToDeterminePayerCountry,
)
from ..i18n.currencies import Money
from ..website import website
from .common import (
Expand Down Expand Up @@ -181,6 +183,35 @@ def capture_order(db, payin):

Doc: https://developer.paypal.com/docs/api/orders/v2/#orders_capture
"""
# Check the country the payment is coming from, if a recipient cares
limited_recipients = db.all("""
SELECT recipient_p
FROM payin_transfers pt
JOIN recipient_settings rs ON rs.participant = pt.recipient
JOIN participants recipient_p ON recipient_p.id = pt.recipient
WHERE pt.payin = %s
AND rs.patron_countries IS NOT NULL
""", (payin.id,))
if limited_recipients:
url = 'https://api.%s/v2/checkout/orders/%s' % (
website.app_conf.paypal_domain, payin.remote_id
)
response = _init_session().get(url)
if response.status_code != 200:
raise PaymentError('PayPal')
order = response.json()
payer_country = order.get('payer', {}).get('address', {}).get('country_code')
if not payer_country:
raise UnableToDeterminePayerCountry()
for recipient in limited_recipients:
if (allowed_countries := recipient.recipient_settings.patron_countries):
if payer_country not in allowed_countries:
state = website.state.get()
_, locale = state['_'], state['locale']
error = ProhibitedSourceCountry(recipient, payer_country).msg(_, locale)
error += " (error code: ProhibitedSourceCountry)"
return abort_payin(db, payin, error)
# Ask PayPal to settle the payment
url = 'https://api.%s/v2/checkout/orders/%s/capture' % (
website.app_conf.paypal_domain, payin.remote_id
)
Expand Down
3 changes: 3 additions & 0 deletions sql/branch.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE recipient_settings
ALTER COLUMN patron_visibilities DROP NOT NULL,
ADD COLUMN patron_countries text CHECK (patron_countries <> '');
8 changes: 8 additions & 0 deletions style/base/columns.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
column-count: 2;
column-gap: 2ex;
}
.columns-sm-3 {
column-count: 3;
column-gap: 1.5ex;
}
}
@media (min-width: $screen-md-min) {
.columns-md-3 {
Expand All @@ -13,4 +17,8 @@
column-count: 4;
column-gap: 2ex;
}
.columns-md-5 {
column-count: 5;
column-gap: 1.5ex;
}
}
8 changes: 8 additions & 0 deletions style/base/lists.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
.checklist {
list-style: none;
padding-left: 0;
& > li {
padding-left: 0;
}
}

.right-pointing-arrows {
list-style-type: '→ ';
padding-left: 2ex;
Expand Down
1 change: 1 addition & 0 deletions templates/macros/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
('/username', _("Name")),
('/avatar', _("Avatar")),
('/currencies', _("Currencies")),
('/countries', _("Countries")),
('/goal', _("Goal")),
('/statement', _("Descriptions")),
('/elsewhere', _("Linked Accounts")),
Expand Down
13 changes: 12 additions & 1 deletion templates/macros/your-tip.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
% if request.qs.get('currency') in accepted_currencies
% set new_currency = request.qs['currency']
% endif
% if not tippee_is_stub
% set patron_countries = tippee.recipient_settings.patron_countries
% set source_country = request.source_country
% if patron_countries and source_country and source_country not in patron_countries
<p class="alert alert-warning">{{ _(
"It looks like you are in {country}. {username} does not accept "
"donations coming from that country.",
country=locale.Country(source_country), username=tippee_name,
) }}</p>
% endif
% endif
% set currency_mismatch = tip_currency not in accepted_currencies
% if tip.renewal_mode > 0 and not pledging
% if currency_mismatch
Expand Down Expand Up @@ -228,7 +239,7 @@ <h5 class="list-group-item-heading">{{ _("Manual renewal") }}</h5>
% macro tip_visibility_choice(tippee_name, patron_visibilities, payment_providers, tip)
% set paypal_only = payment_providers == 2
% if paypal_only
% if patron_visibilities == 0
% if not patron_visibilities
% set patron_visibilities = 2
% elif patron_visibilities.__and__(1)
% set patron_visibilities = patron_visibilities.__xor__(1).__or__(2)
Expand Down
Loading
Loading