diff --git a/liberapay/models/exchange_route.py b/liberapay/models/exchange_route.py
index 33edb0e15..02120b1de 100644
--- a/liberapay/models/exchange_route.py
+++ b/liberapay/models/exchange_route.py
@@ -223,6 +223,22 @@ def set_as_default(self):
id=self.id, network=self.network
))
+ def set_as_default_for(self, currency):
+ with self.db.get_cursor() as cursor:
+ cursor.run("""
+ UPDATE exchange_routes
+ SET is_default_for = NULL
+ WHERE participant = %(p_id)s
+ AND is_default_for = %(currency)s;
+ UPDATE exchange_routes
+ SET is_default_for = %(currency)s
+ WHERE participant = %(p_id)s
+ AND id = %(route_id)s
+ """, dict(p_id=self.participant.id, route_id=self.id, currency=currency))
+ self.participant.add_event(cursor, 'set_default_route_for', dict(
+ id=self.id, network=self.network, currency=currency,
+ ))
+
def set_mandate(self, mandate_id):
self.db.run("""
UPDATE exchange_routes
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
index 2a7846848..5ef449bf1 100644
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -2028,6 +2028,16 @@ def get_currencies_for(tippee, tip):
fallback_currency = 'USD'
return fallback_currency, accepted
+ @cached_property
+ def donates_in_multiple_currencies(self):
+ return self.db.one("""
+ SELECT count(DISTINCT amount::currency) > 1
+ FROM current_tips
+ WHERE tipper = %s
+ AND amount > 0
+ AND renewal_mode > 0
+ """, (self.id,))
+
# More Random Stuff
# =================
diff --git a/liberapay/payin/cron.py b/liberapay/payin/cron.py
index b755e0eab..50eeac7aa 100644
--- a/liberapay/payin/cron.py
+++ b/liberapay/payin/cron.py
@@ -264,7 +264,8 @@ def execute_scheduled_payins():
AND r.status = 'chargeable'
AND r.network::text LIKE 'stripe-%%'
AND ( sp.amount::currency = 'EUR' OR r.network <> 'stripe-sdd' )
- ORDER BY r.is_default NULLS LAST
+ ORDER BY r.is_default_for = sp.amount::currency DESC NULLS LAST
+ , r.is_default DESC NULLS LAST
, r.ctime DESC
LIMIT 1
) r ON true
diff --git a/sql/branch.sql b/sql/branch.sql
new file mode 100644
index 000000000..3162afb92
--- /dev/null
+++ b/sql/branch.sql
@@ -0,0 +1 @@
+ALTER TABLE exchange_routes ADD COLUMN is_default_for currency;
diff --git a/www/%username/giving/pay/stripe/%payin_id.spt b/www/%username/giving/pay/stripe/%payin_id.spt
index 66ae4d4ad..e1db9b691 100644
--- a/www/%username/giving/pay/stripe/%payin_id.spt
+++ b/www/%username/giving/pay/stripe/%payin_id.spt
@@ -87,19 +87,20 @@ if request.method == 'POST':
body.word('token'), one_off, payin_amount, owner_info, return_url
)
route = ExchangeRoute.attach_stripe_source(payer, source, one_off)
- if body.parse_boolean('set_as_default', default=False):
- route.set_as_default()
elif 'stripe_pm_id' in body:
one_off = body.get('keep') != 'true'
pm = stripe.PaymentMethod.retrieve(body.word('stripe_pm_id'))
route = ExchangeRoute.attach_stripe_payment_method(payer, pm, one_off)
- if body.parse_boolean('set_as_default', default=False):
- route.set_as_default()
else:
route = ExchangeRoute.from_id(payer, body.get_int('route'), _raise=False)
if route is None:
raise response.invalid_input(body.get('route'), 'route', 'body')
route.sync_status()
+ if body.parse_boolean('set_as_default', default=False):
+ route.set_as_default()
+ set_as_default_for = body.get_currency('set_as_default_for', None)
+ if set_as_default_for:
+ route.set_as_default_for(set_as_default_for)
except stripe.error.StripeError as e:
raise response.error(e.http_status or 500, _(
@@ -188,7 +189,20 @@ elif payin_id:
""", (payin.id,)) > 0
)
-else:
+tippees = request.qs.parse_list('beneficiary', int, default=None)
+if tippees:
+ tips = [
+ tip for tip in payer.get_tips_to(tippees)
+ if tip.tippee_p.payment_providers & STRIPE_BIT > 0
+ ]
+ if len(set(tip.amount.currency for tip in tips)) != 1:
+ raise response.invalid_input(tippees, 'beneficiary', 'querystring')
+ payment = PayinProspect(payer, tips, 'stripe')
+ for tip in tips:
+ if payment.currency not in tip.tippee_p.accepted_currencies_set:
+ raise response.redirect(payer.path(
+ 'giving/pay?redirect_reason=unaccepted_currency'
+ ))
routes = website.db.all("""
SELECT r
FROM exchange_routes r
@@ -196,10 +210,11 @@ else:
AND r.status = 'chargeable'
AND r.network::text LIKE %s
AND (r.one_off IS FALSE OR r.ctime > (current_timestamp - interval '6 hours'))
- ORDER BY r.is_default NULLS LAST
+ ORDER BY r.is_default_for = %s DESC NULLS LAST
+ , r.is_default DESC NULLS LAST
, r.network = 'stripe-sdd' DESC
, r.id DESC
- """, (payer.id, 'stripe-' + (payment_type or '%')))
+ """, (payer.id, 'stripe-' + (payment_type or '%'), payment.currency))
if routes:
route = None
while routes:
@@ -212,21 +227,6 @@ else:
del route
if not payment_type:
response.redirect(payer.path('giving/pay'))
-
-tippees = request.qs.parse_list('beneficiary', int, default=None)
-if tippees:
- tips = [
- tip for tip in payer.get_tips_to(tippees)
- if tip.tippee_p.payment_providers & STRIPE_BIT > 0
- ]
- if len(set(tip.amount.currency for tip in tips)) != 1:
- raise response.invalid_input(tippees, 'beneficiary', 'querystring')
- payment = PayinProspect(payer, tips, 'stripe')
- for tip in tips:
- if payment.currency not in tip.tippee_p.accepted_currencies_set:
- raise response.redirect(payer.path(
- 'giving/pay?redirect_reason=unaccepted_currency'
- ))
ask_for_postal_address = (
payment_type == 'sdd' or
len(tips) == 1 and
@@ -509,10 +509,18 @@ title = _("Funding your donations")
{{ _("Remember the card number for next time") }}
+ % if payer.donates_in_multiple_currencies
+
+ % else
+ % endif
% elif payment_type == 'sdd'
diff --git a/www/%username/routes/index.spt b/www/%username/routes/index.spt
index b916f3482..d43d476ce 100644
--- a/www/%username/routes/index.spt
+++ b/www/%username/routes/index.spt
@@ -19,6 +19,17 @@ if request.method == 'POST':
route = ExchangeRoute.from_id(participant, request.body.get_int('set_as_default'), _raise=False)
if route:
route.set_as_default()
+ elif 'set_as_default_for' in request.body:
+ try:
+ route_id, currency = request.body['set_as_default_for'].split(':')
+ route_id = int(route_id)
+ if currency not in constants.CURRENCIES:
+ raise ValueError(currency)
+ except ValueError:
+ raise response.invalid_input(request.body['set_as_default_for'], 'set_as_default_for', 'body')
+ route = ExchangeRoute.from_id(participant, route_id, _raise=False)
+ if route:
+ route.set_as_default_for(currency)
else:
raise response.error(400, "no known action found in request body")
form_post_success(state)
@@ -100,7 +111,16 @@ title = _("Payment Instruments")
({{ locale.currencies.get(route.currency, route.currency) }})
% endif
% if route.is_default
- {{ _("default") }}
+ {{ _("default") }}
+ % elif route.is_default_for
+ {{ _(
+ "default for {currency}", currency=route.is_default_for
+ ) }}
% endif
% if route.status == 'pending'
{{ _("pending") }}
@@ -159,6 +179,16 @@ title = _("Payment Instruments")
% if not route.is_default
+ % if last_payin and participant.donates_in_multiple_currencies
+
+ % endif
% endif