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