diff --git a/liberapay/constants.py b/liberapay/constants.py index aca11451a9..c4474e77e8 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -1,9 +1,9 @@ # coding: utf8 from __future__ import print_function, unicode_literals -from collections import namedtuple, OrderedDict +from collections import defaultdict, namedtuple, OrderedDict from datetime import date, datetime, timedelta -from decimal import Decimal, ROUND_UP +from decimal import Decimal, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP import re from jinja2 import StrictUndefined @@ -47,6 +47,34 @@ def with_vat(self): return r[0] if not r[1] else r[1].round_up() if not r[0] else r +def to_precision(x, precision, rounding=ROUND_HALF_UP): + if not x: + return x + # round + factor = Decimal(10) ** (x.log10().to_integral(ROUND_FLOOR) + 1) + r = (x / factor).quantize(Decimal(10) ** -precision, rounding=rounding) * factor + # remove trailing zeros + r = r.quantize(Decimal(10) ** (int(x.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)] + return to_precision(amount * rate, precision, rounding) + + +class MoneyAutoConvertDict(defaultdict): + + def __init__(self, *args, **kw): + super(MoneyAutoConvertDict, self).__init__(None, *args, **kw) + + def __missing__(self, currency): + r = Money(convert_symbolic_amount(self['EUR'].amount, currency, 1), currency) + self[currency] = r + return r + + StandardTip = namedtuple('StandardTip', 'label weekly monthly yearly') @@ -64,13 +92,29 @@ def with_vat(self): BIRTHDAY = date(2015, 5, 22) -CURRENCIES = ordered_set(['EUR', 'USD']) +CURRENCIES = ordered_set([ + 'EUR', 'USD', + 'AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'GBP', 'HKD', 'HRK', + 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', + 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR' +]) D_CENT = Decimal('0.01') D_INF = Decimal('inf') D_MAX = Decimal('999999999999.99') D_ZERO = Decimal('0.00') +class _DonationLimits(defaultdict): + def __missing__(self, currency): + r = { + period: ( + Money(convert_symbolic_amount(eur_amounts[0], currency, rounding=ROUND_UP), currency), + Money(convert_symbolic_amount(eur_amounts[1], currency, rounding=ROUND_UP), currency) + ) for period, eur_amounts in DONATION_LIMITS_EUR_USD.items() + } + self[currency] = r + return r + DONATION_LIMITS_WEEKLY_EUR_USD = (Decimal('0.01'), Decimal('100.00')) DONATION_LIMITS_EUR_USD = { 'weekly': DONATION_LIMITS_WEEKLY_EUR_USD, @@ -79,10 +123,10 @@ def with_vat(self): 'yearly': tuple((x * Decimal(52)).quantize(D_CENT) for x in DONATION_LIMITS_WEEKLY_EUR_USD), } -DONATION_LIMITS = { +DONATION_LIMITS = _DonationLimits(None, { '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()}, -} +}) DOMAIN_RE = re.compile(r''' ^ @@ -232,39 +276,39 @@ def with_vat(self): } PAYIN_DIRECT_DEBIT_MAX = {k: Money('2500.00', k) for k in ('EUR', 'USD')} -PAYIN_PAYPAL_MIN_ACCEPTABLE = { # fee > 10% +PAYIN_PAYPAL_MIN_ACCEPTABLE = MoneyAutoConvertDict({ # fee > 10% 'EUR': Money('2.00', 'EUR'), 'USD': Money('2.00', 'USD'), -} -PAYIN_PAYPAL_MIN_RECOMMENDED = { # fee < 8% +}) +PAYIN_PAYPAL_MIN_RECOMMENDED = MoneyAutoConvertDict({ # fee < 8% 'EUR': Money('10.00', 'EUR'), 'USD': Money('12.00', 'USD'), -} -PAYIN_PAYPAL_LOW_FEE = { # fee < 6% +}) +PAYIN_PAYPAL_LOW_FEE = MoneyAutoConvertDict({ # fee < 6% 'EUR': Money('40.00', 'EUR'), 'USD': Money('48.00', 'USD'), -} -PAYIN_PAYPAL_MAX_ACCEPTABLE = { +}) +PAYIN_PAYPAL_MAX_ACCEPTABLE = MoneyAutoConvertDict({ 'EUR': Money('5000.00', 'EUR'), 'USD': Money('5000.00', 'USD'), -} +}) -PAYIN_STRIPE_MIN_ACCEPTABLE = { # fee > 10% +PAYIN_STRIPE_MIN_ACCEPTABLE = MoneyAutoConvertDict({ # fee > 10% 'EUR': Money('2.00', 'EUR'), 'USD': Money('2.00', 'USD'), -} -PAYIN_STRIPE_MIN_RECOMMENDED = { # fee < 8% +}) +PAYIN_STRIPE_MIN_RECOMMENDED = MoneyAutoConvertDict({ # fee < 8% 'EUR': Money('10.00', 'EUR'), 'USD': Money('12.00', 'USD'), -} -PAYIN_STRIPE_LOW_FEE = { # fee < 6% +}) +PAYIN_STRIPE_LOW_FEE = MoneyAutoConvertDict({ # fee < 6% 'EUR': Money('40.00', 'EUR'), 'USD': Money('48.00', 'USD'), -} -PAYIN_STRIPE_MAX_ACCEPTABLE = { +}) +PAYIN_STRIPE_MAX_ACCEPTABLE = MoneyAutoConvertDict({ 'EUR': Money('5000.00', 'EUR'), 'USD': Money('5000.00', 'USD'), -} +}) PAYMENT_METHODS = { 'mango-ba': _("Direct Debit"), @@ -366,6 +410,17 @@ def make_standard_tip(label, weekly, currency): ) +class _StandardTips(defaultdict): + def __missing__(self, currency): + r = [ + make_standard_tip( + label, convert_symbolic_amount(weekly, currency, rounding=ROUND_UP), currency + ) for label, weekly in STANDARD_TIPS_EUR_USD + ] + self[currency] = r + return r + + STANDARD_TIPS_EUR_USD = ( (_("Symbolic"), Decimal('0.01')), (_("Small"), Decimal('0.25')), @@ -373,10 +428,10 @@ def make_standard_tip(label, weekly, currency): (_("Large"), Decimal('5.00')), (_("Maximum"), DONATION_LIMITS_EUR_USD['weekly'][1]), ) -STANDARD_TIPS = { +STANDARD_TIPS = _StandardTips(None, { '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], -} +}) SUMMARY_MAX_SIZE = 100 diff --git a/liberapay/utils/currencies.py b/liberapay/utils/currencies.py index 8e97ec4af3..8e405b581b 100644 --- a/liberapay/utils/currencies.py +++ b/liberapay/utils/currencies.py @@ -192,7 +192,7 @@ def __sub__(self, other): def __repr__(self): return '%s[%s]' % ( self.__class__.__name__, - ', '.join('%s %s' % (a, c) for c, a in self.amounts.items()) + ', '.join('%s %s' % (a, c) for c, a in self.amounts.items() if a) ) def __bool__(self): diff --git a/templates/currencies.html b/templates/currencies.html index 9f7ca9d1d8..6fb6a2d96a 100644 --- a/templates/currencies.html +++ b/templates/currencies.html @@ -1,14 +1,10 @@ % macro currency_buttons(name, selected) -
- % for c in constants.CURRENCIES - + % endmacro diff --git a/templates/your-tip.html b/templates/your-tip.html index 82f3141e2e..076ca5806c 100644 --- a/templates/your-tip.html +++ b/templates/your-tip.html @@ -1,15 +1,18 @@ % macro tip_form(tippee, tip=None, inline=False, disabled='') % set pledging = tippee.__class__.__name__ == 'AccountElsewhere' + % set tippee_name = tippee.friendly_name if pledging else tippee.username % set tip = tip or user.get_tip_to(tippee.participant or tippee, currency) % set tip_currency = tip.amount.currency % set new_currency, accepted_currencies = user.get_currencies_for(tippee, tip) + % if request.qs.get('currency') in accepted_currencies + % set new_currency = request.qs['currency'] + % endif % set currency_mismatch = tip.amount > 0 and tip_currency not in accepted_currencies % if inline
% if currency_mismatch - % set tippee_name = tippee.friendly_name if pledging else tippee.username

{{ _( "You are currently donating {money_amount} per week to {name}, " "but they no longer accept donations in {currency}. You can " @@ -32,7 +35,6 @@

% else % if tip.amount > 0 - % set tippee_name = tippee.friendly_name if pledging else tippee.username % if currency_mismatch

{{ _( "You are currently donating {money_amount} per week to {name}, but " @@ -53,30 +55,34 @@ % else

{{ _("Please select or input an amount:") }}

% endif -
- % for c in constants.CURRENCIES - % set is_accepted = c in accepted_currencies - - - % if is_accepted -
- - - -
- {{ tip_select(tip, c, tippee, disabled, pledging=pledging) }} -
+ + + +
+ {{ tip_select(tip, new_currency, tippee, disabled, pledging=pledging) }} +
+
+ % if len(accepted_currencies) > 1 +
+

{{ ngettext( + "Wrong currency? {username} also accepts {n} other:", + "Wrong currency? {username} also accepts {n} others:", + n=len(accepted_currencies) - 1, username=tippee_name + ) }}

+
+ +
- % endif - % endfor -
+ % endif % endif % endmacro diff --git a/tests/py/test_history.py b/tests/py/test_history.py index b952e9cba0..1d024bf2f6 100644 --- a/tests/py/test_history.py +++ b/tests/py/test_history.py @@ -5,7 +5,7 @@ from liberapay.billing.payday import Payday from liberapay.models.participant import Participant -from liberapay.testing import EUR, USD, Harness +from liberapay.testing import EUR, Harness from liberapay.testing.mangopay import FakeTransfersHarness from liberapay.utils.history import ( get_end_of_period_balances, get_start_of_current_utc_day, get_wallet_ledger @@ -136,7 +136,7 @@ def test_get_end_of_period_balances(self): today = get_start_of_current_utc_day() period_end = today.replace(month=1, day=1) balances = get_end_of_period_balances(self.db, self.alice, period_end, today) - assert list(balances) == [EUR('10.00'), USD('0.00')] + assert balances == EUR('10.00') class TestExport(Harness): diff --git a/www/%username/donate.spt b/www/%username/donate.spt index 0c4a0ea602..c938092a7e 100644 --- a/www/%username/donate.spt +++ b/www/%username/donate.spt @@ -77,6 +77,14 @@ full_title = _("Donate to {0} via Liberapay", participant.username)

{{ _("Frequently Asked Questions") }}

+ % if len(participant.accepted_currencies) == 1 +

{{ _("Can I donate in another currency?") }}

+

{{ _( + "No, {username} only accepts payments in {currency}.", + username=participant.username, currency=Currency(participant.main_currency) + ) }}

+ % endif +

{{ _("What payment methods are available?") }}

{{ _( "We currently support most credit and debit cards (Visa, MasterCard, American Express). " diff --git a/www/%username/tip.spt b/www/%username/tip.spt index eb71951420..7c91191bde 100644 --- a/www/%username/tip.spt +++ b/www/%username/tip.spt @@ -53,7 +53,7 @@ if tippee.status == 'stub': if request.method == 'POST': currency = request.body.get('currency', 'EUR') - if currency not in constants.STANDARD_TIPS: + if currency not in constants.CURRENCIES: raise response.error(400, "`currency` value in body is invalid or non-supported") amount = request.body.get('selected_amount') if amount and amount != 'custom': diff --git a/www/%username/tips.json.spt b/www/%username/tips.json.spt index 3c071b3e70..7d6b27b9d9 100644 --- a/www/%username/tips.json.spt +++ b/www/%username/tips.json.spt @@ -18,7 +18,7 @@ if request.method == 'POST': seen.add(tip['username']) one = {"username": tip['username']} currency = tip.get('currency', 'EUR') - if currency not in constants.STANDARD_TIPS: + if currency not in constants.CURRENCIES: raise response.error(400,"`currency` value '%s' in body is invalid or non-supported" % currency) try: amount = locale.parse_money_amount(tip['amount'], currency)