From 45f1b2432649e7bd7b08cbe1b553769c0afa97c5 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sat, 14 Oct 2017 17:12:49 +0200 Subject: [PATCH 1/7] adapt standard donation amounts to the currency --- liberapay/constants.py | 24 ++++++++++++++---------- templates/your-tip.html | 12 ++++++------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/liberapay/constants.py b/liberapay/constants.py index 2f570feb7d..1da1159b05 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -218,22 +218,26 @@ def check_bits(bits): SESSION_TIMEOUT = timedelta(hours=6) -def make_standard_tip(label, weekly): +def make_standard_tip(label, weekly, currency): return StandardTip( label, - weekly, - weekly / PERIOD_CONVERSION_RATES['monthly'], - weekly / PERIOD_CONVERSION_RATES['yearly'], + Money(weekly, currency), + Money(weekly / PERIOD_CONVERSION_RATES['monthly'], currency), + Money(weekly / PERIOD_CONVERSION_RATES['yearly'], currency), ) -STANDARD_TIPS = ( - make_standard_tip(_("Symbolic"), Decimal('0.01')), - make_standard_tip(_("Small"), Decimal('0.25')), - make_standard_tip(_("Medium"), Decimal('1.00')), - make_standard_tip(_("Large"), Decimal('5.00')), - make_standard_tip(_("Maximum"), DONATION_WEEKLY_MAX), +STANDARD_TIPS_EUR_USD = ( + (_("Symbolic"), Decimal('0.01')), + (_("Small"), Decimal('0.25')), + (_("Medium"), Decimal('1.00')), + (_("Large"), Decimal('5.00')), + (_("Maximum"), DONATION_WEEKLY_MAX), ) +STANDARD_TIPS = { + '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/templates/your-tip.html b/templates/your-tip.html index eb1c5ae3f6..d911e8cb81 100644 --- a/templates/your-tip.html +++ b/templates/your-tip.html @@ -26,7 +26,7 @@ % macro tip_input(tip, disabled='', small=False)
-
+
{{ locale.currency_symbols.get(currency, currency) }}
{{ _("Please select or input an amount:") }}

% endif
    - % for std_tip in constants.STANDARD_TIPS + % for std_tip in constants.STANDARD_TIPS[currency] % set amount_is_a_match = std_tip.weekly == tip.amount % if amount_is_a_match {{ tip_is_standard.append(True) or '' }} % endif
  • From a2036d411d0258d56bf89eb95510bfbc74329c0b Mon Sep 17 00:00:00 2001 From: Changaco Date: Sat, 14 Oct 2017 17:45:39 +0200 Subject: [PATCH 2/7] adapt donation limits to the currency --- liberapay/constants.py | 17 ++++++++++------- liberapay/exceptions.py | 6 +++--- liberapay/models/participant.py | 8 +++++--- liberapay/utils/fake_data.py | 5 +++-- www/about/faq.spt | 4 ++-- www/about/index.spt | 2 +- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/liberapay/constants.py b/liberapay/constants.py index 1da1159b05..7ed5980dae 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -55,15 +55,18 @@ def check_bits(bits): D_UNIT = Decimal('1.00') D_ZERO = Decimal('0.00') -DONATION_LIMITS_WEEKLY = (Decimal('0.01'), Decimal('100.00')) -DONATION_LIMITS = { - 'weekly': DONATION_LIMITS_WEEKLY, +DONATION_LIMITS_WEEKLY_EUR_USD = (Decimal('0.01'), Decimal('100.00')) +DONATION_LIMITS_EUR_USD = { + 'weekly': DONATION_LIMITS_WEEKLY_EUR_USD, 'monthly': tuple((x * Decimal(52) / Decimal(12)).quantize(D_CENT, rounding=ROUND_UP) - for x in DONATION_LIMITS_WEEKLY), + for x in DONATION_LIMITS_WEEKLY_EUR_USD), 'yearly': tuple((x * Decimal(52)).quantize(D_CENT) - for x in DONATION_LIMITS_WEEKLY), + for x in DONATION_LIMITS_WEEKLY_EUR_USD), +} +DONATION_LIMITS = { + '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()}, } -DONATION_WEEKLY_MIN, DONATION_WEEKLY_MAX = DONATION_LIMITS_WEEKLY DOMAIN_RE = re.compile(r''' ^ @@ -232,7 +235,7 @@ def make_standard_tip(label, weekly, currency): (_("Small"), Decimal('0.25')), (_("Medium"), Decimal('1.00')), (_("Large"), Decimal('5.00')), - (_("Maximum"), DONATION_WEEKLY_MAX), + (_("Maximum"), DONATION_LIMITS_EUR_USD['weekly'][1]), ) STANDARD_TIPS = { 'EUR': [make_standard_tip(label, weekly, 'EUR') for label, weekly in STANDARD_TIPS_EUR_USD], diff --git a/liberapay/exceptions.py b/liberapay/exceptions.py index 11e74a662d..abaf779bfe 100644 --- a/liberapay/exceptions.py +++ b/liberapay/exceptions.py @@ -3,7 +3,7 @@ from dependency_injection import resolve_dependencies from pando import Response -from .constants import DONATION_LIMITS, PASSWORD_MIN_SIZE, PASSWORD_MAX_SIZE +from .constants import PASSWORD_MIN_SIZE, PASSWORD_MAX_SIZE class Redirect(Exception): @@ -202,8 +202,8 @@ def msg(self, _): class BadAmount(LazyResponse400): def msg(self, _): - period = self.args[1] - return _(BAD_AMOUNT_MESSAGES[period], self.args[0], *DONATION_LIMITS[period]) + amount, period, limits = self.args + return _(BAD_AMOUNT_MESSAGES[period], amount, *limits) class UserDoesntAcceptTips(LazyResponseXXX): code = 403 diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index b830b12a7a..0ef2f55a20 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -21,7 +21,7 @@ from liberapay.constants import ( ASCII_ALLOWED_IN_USERNAME, AVATAR_QUERY, D_CENT, D_ZERO, - DONATION_WEEKLY_MAX, DONATION_WEEKLY_MIN, EMAIL_RE, + DONATION_LIMITS, EMAIL_RE, EMAIL_VERIFICATION_TIMEOUT, EVENTS, PASSWORD_MAX_SIZE, PASSWORD_MIN_SIZE, PERIOD_CONVERSION_RATES, PRIVILEGES, SESSION, SESSION_REFRESH, SESSION_TIMEOUT, USERNAME_MAX_SIZE @@ -1562,8 +1562,10 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', periodic_amount = Decimal(periodic_amount) # May raise InvalidOperation amount = periodic_amount * PERIOD_CONVERSION_RATES[period] - if periodic_amount != 0 and amount < DONATION_WEEKLY_MIN or amount > DONATION_WEEKLY_MAX: - raise BadAmount(periodic_amount, period) + if periodic_amount != 0: + limits = DONATION_LIMITS['EUR']['weekly'] # TODO + if amount < limits[0] or amount > limits[1]: + raise BadAmount(periodic_amount, period, limits) amount = amount.quantize(D_CENT, rounding=ROUND_UP) diff --git a/liberapay/utils/fake_data.py b/liberapay/utils/fake_data.py index 9216734584..e5585912aa 100644 --- a/liberapay/utils/fake_data.py +++ b/liberapay/utils/fake_data.py @@ -90,7 +90,8 @@ def fake_tip(db, tipper, tippee): """Create a fake tip. """ period = random.choice(DONATION_PERIODS) - periodic_amount = random_money_amount(*DONATION_LIMITS[period]) + limits = [l.amount for l in DONATION_LIMITS['EUR'][period]] + periodic_amount = random_money_amount(*limits) amount = (periodic_amount * PERIOD_CONVERSION_RATES[period]).quantize(D_CENT) return _fake_thing( db, @@ -222,7 +223,7 @@ def populate_db(website, num_participants=100, num_tips=200, num_teams=5, num_tr tips.append(fake_tip(db, tipper, tippee)) # Transfers - min_amount, max_amount = DONATION_LIMITS['weekly'] + min_amount, max_amount = [l.amount for l in DONATION_LIMITS['EUR']['weekly']] transfers = [] for i in range(num_transfers): tipper, tippee = random.sample(participants, 2) diff --git a/www/about/faq.spt b/www/about/faq.spt index 8774f6773e..1e582a3bea 100644 --- a/www/about/faq.spt +++ b/www/about/faq.spt @@ -117,13 +117,13 @@ title = _("Frequently Asked Questions") "The minimum you can give any user is {0}. To minimize processing fees, " "we charge your credit card at least {1} at a time (the money is stored " "in your Liberapay wallet and transferred to others during Payday)." - , Money(constants.DONATION_WEEKLY_MIN, 'EUR') + , constants.DONATION_LIMITS[currency]['weekly'][0] , Money(constants.PAYIN_CARD_MIN, 'EUR') ) }}
    {{ _( "The maximum you can give any one user is {0} per week. This helps to " "stabilize income by reducing how dependent it is on a few large patrons." - , Money(constants.DONATION_WEEKLY_MAX, 'EUR') + , constants.DONATION_LIMITS[currency]['weekly'][1] ) }} diff --git a/www/about/index.spt b/www/about/index.spt index b5a994fd18..49a971d4f8 100644 --- a/www/about/index.spt +++ b/www/about/index.spt @@ -15,7 +15,7 @@ title = _("Introduction") "Payments come with no strings attached. You don't know exactly who is " "giving to you, and donations are capped at {0} per week per donor to " "dampen undue influence." - , Money(constants.DONATION_WEEKLY_MAX, 'EUR') + , constants.DONATION_LIMITS[currency]['weekly'][1] ) }}

    {{ _( From c0ded5c6fe29cce7a6a266edda3d049775021afe Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 15 Oct 2017 11:55:06 +0200 Subject: [PATCH 3/7] modify set_tip_to() for currencies --- liberapay/exceptions.py | 10 ++++++++++ liberapay/models/participant.py | 27 +++++++++++++++++---------- www/%username/tip.spt | 4 ++++ www/%username/tips.json.spt | 10 +++++++--- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/liberapay/exceptions.py b/liberapay/exceptions.py index abaf779bfe..54ffeab8ea 100644 --- a/liberapay/exceptions.py +++ b/liberapay/exceptions.py @@ -210,6 +210,16 @@ class UserDoesntAcceptTips(LazyResponseXXX): def msg(self, _): return _("The user {0} doesn't accept donations.", *self.args) +class BadDonationCurrency(LazyResponseXXX): + code = 403 + def msg(self, _): + tippee, rejected_currency = self.args + return _( + "Donations to {username} must be in {main_currency}, not {rejected_currency}.", + username=tippee.username, main_currency=tippee.main_currency, + rejected_currency=rejected_currency, + ) + class NonexistingElsewhere(LazyResponse400): def msg(self, _): diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index 0ef2f55a20..f8563792ec 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -1,7 +1,7 @@ from __future__ import print_function, unicode_literals from base64 import b64decode, b64encode -from decimal import Decimal, ROUND_DOWN, ROUND_UP +from decimal import ROUND_DOWN, ROUND_UP from email.utils import formataddr from hashlib import pbkdf2_hmac, md5 from os import urandom @@ -13,6 +13,7 @@ import aspen_jinja2_renderer from html2text import html2text import mangopay +from mangopay.utils import Money from markupsafe import escape as htmlescape from pando.utils import utcnow from postgres.orm import Model @@ -28,6 +29,7 @@ ) from liberapay.exceptions import ( BadAmount, + BadDonationCurrency, BadEmailAddress, BadPasswordSize, CannotRemovePrimaryEmail, @@ -540,7 +542,7 @@ def clear_tips_giving(self, cursor): """, (self.id,)) for tippee in tippees: - self.set_tip_to(tippee, '0.00', update_self=False, cursor=cursor) + self.set_tip_to(tippee, Money(0, 'EUR'), update_self=False, cursor=cursor) def clear_tips_receiving(self, cursor): """Zero out tips to a given user. @@ -557,7 +559,7 @@ def clear_tips_receiving(self, cursor): """, (self.id,)) for tipper in tippers: - tipper.set_tip_to(self, '0.00', update_tippee=False, cursor=cursor) + tipper.set_tip_to(self, Money(0, 'EUR'), update_tippee=False, cursor=cursor) def clear_takes(self, cursor): """Leave all teams by zeroing all takes. @@ -1559,18 +1561,23 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', if self.id == tippee.id: raise NoSelfTipping - periodic_amount = Decimal(periodic_amount) # May raise InvalidOperation + if not isinstance(periodic_amount, Money): + # TODO drop this + periodic_amount = Money(periodic_amount, 'EUR') amount = periodic_amount * PERIOD_CONVERSION_RATES[period] if periodic_amount != 0: - limits = DONATION_LIMITS['EUR']['weekly'] # TODO + limits = DONATION_LIMITS[amount.currency]['weekly'] if amount < limits[0] or amount > limits[1]: raise BadAmount(periodic_amount, period, limits) - amount = amount.quantize(D_CENT, rounding=ROUND_UP) + amount = amount.round_up() - if not tippee.accepts_tips and amount != 0: - raise UserDoesntAcceptTips(tippee.username) + if amount != 0: + if not tippee.accepts_tips: + raise UserDoesntAcceptTips(tippee.username) + if not tippee.accept_all_currencies and amount.currency != tippee.main_currency: + raise BadDonationCurrency(tippee, amount.currency) # Insert tip t = (cursor or self.db).one("""\ @@ -1588,8 +1595,8 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', , ( SELECT count(*) = 0 FROM tips WHERE tipper=%(tipper)s ) AS first_time_tipper , ( SELECT join_time IS NULL FROM participants WHERE id = %(tippee)s ) AS is_pledge - """, dict(tipper=self.id, tippee=tippee.id, amount=amount, - period=period, periodic_amount=periodic_amount))._asdict() + """, dict(tipper=self.id, tippee=tippee.id, amount=amount.amount, + period=period, periodic_amount=periodic_amount.amount))._asdict() if update_self: # Update giving amount of tipper diff --git a/www/%username/tip.spt b/www/%username/tip.spt index 9fa9a2f79e..b16ba89852 100644 --- a/www/%username/tip.spt +++ b/www/%username/tip.spt @@ -55,6 +55,10 @@ if request.method == 'POST': amount = parse_decimal(amount) else: raise response.error(400, _("The donation amount is missing.")) + currency = request.body.get('currency', 'EUR') + if currency not in constants.STANDARD_TIPS: + raise response.error(400, "`currency` value in body is invalid or non-supported") + amount = Money(amount, currency) out = tipper.set_tip_to(tippee, amount, period) if out['amount'] == 0: out["msg"] = _("Your donation to {0} has been stopped.", tippee_name) diff --git a/www/%username/tips.json.spt b/www/%username/tips.json.spt index 539acf5cfc..ded270529e 100644 --- a/www/%username/tips.json.spt +++ b/www/%username/tips.json.spt @@ -14,21 +14,25 @@ if request.method == 'POST': for tip in new_tips: seen.add(tip['username']) one = {"username": tip['username']} + currency = tip.get('currency', 'EUR') + if currency not in constants.STANDARD_TIPS: + raise response.error(400,"`currency` value '%s' in body is invalid or non-supported" % currency) try: + amount = Money(parse_decimal(tip['amount']), currency) amount = participant.set_tip_to( - tip['username'], parse_decimal(tip['amount']), tip['period'], + tip['username'], amount, tip['period'], )['amount'] except Exception as exc: amount = "error" one['error'] = exc.__class__.__name__ - one['amount'] = str(amount) + one['amount'] = amount out.append(one) if request.qs.get('also_prune', 'false').lower() in ('true', '1', 'yes'): old_tips = participant.get_current_tips() for tip in old_tips: if tip['username'] not in seen: - participant.set_tip_to(tip['username'], '0.00', tip['period']) + participant.set_tip_to(tip['username'], Money(0, 'EUR'), tip['period']) else: tips, total, pledges, pledges_total = participant.get_giving_for_profile() From f6376ee24c6ff13bc4f34c6e227335d3a704c790 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 15 Oct 2017 12:07:31 +0200 Subject: [PATCH 4/7] update set_tip_to() calls in tests --- liberapay/models/participant.py | 3 -- liberapay/testing/__init__.py | 5 ++ tests/py/test_callbacks.py | 7 +-- tests/py/test_charts_json.py | 19 +++---- tests/py/test_close.py | 65 ++++++++++++------------ tests/py/test_elsewhere.py | 4 +- tests/py/test_email_notifs.py | 5 +- tests/py/test_history.py | 6 +-- tests/py/test_pages.py | 14 +++--- tests/py/test_participant.py | 62 +++++++++++------------ tests/py/test_payday.py | 88 ++++++++++++++++----------------- tests/py/test_payio.py | 10 ++-- tests/py/test_public_json.py | 22 ++++----- tests/py/test_stats.py | 16 +++--- tests/py/test_take.py | 18 +++---- 15 files changed, 175 insertions(+), 169 deletions(-) diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index f8563792ec..eac8355272 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -1561,9 +1561,6 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly', if self.id == tippee.id: raise NoSelfTipping - if not isinstance(periodic_amount, Money): - # TODO drop this - periodic_amount = Money(periodic_amount, 'EUR') amount = periodic_amount * PERIOD_CONVERSION_RATES[period] if periodic_amount != 0: diff --git a/liberapay/testing/__init__.py b/liberapay/testing/__init__.py index ba6ae608e2..ca53cb9267 100644 --- a/liberapay/testing/__init__.py +++ b/liberapay/testing/__init__.py @@ -8,6 +8,7 @@ from os.path import dirname, join, realpath from aspen import resources +from mangopay.utils import Money from pando.utils import utcnow from pando.testing.client import Client from psycopg2 import IntegrityError, InternalError @@ -31,6 +32,10 @@ PROJECT_ROOT = str(TOP) +def EUR(amount): + return Money(amount, 'EUR') + + class ClientWithAuth(Client): def __init__(self, *a, **kw): diff --git a/tests/py/test_callbacks.py b/tests/py/test_callbacks.py index 26efb5ecd9..5fef2696e9 100644 --- a/tests/py/test_callbacks.py +++ b/tests/py/test_callbacks.py @@ -11,6 +11,7 @@ from liberapay.billing.transactions import Money, record_exchange from liberapay.models.exchange_route import ExchangeRoute from liberapay.security.csrf import CSRF_TOKEN +from liberapay.testing import EUR from liberapay.testing.emails import EmailHarness from liberapay.testing.mangopay import fake_transfer, FakeTransfersHarness, MangopayHarness from liberapay.utils import utcnow @@ -43,7 +44,7 @@ def test_dispute_callback_lost(self, save, get_payin, get_dispute): payin = PayIn(tag=str(e_id)) get_payin.return_value = payin # Transfer some of the money to homer - self.janet.set_tip_to(self.homer, D('3.68')) + self.janet.set_tip_to(self.homer, EUR('3.68')) Payday.start().run() # Withdraw some of the money self.make_exchange('mango-ba', D('-2.68'), 0, self.homer) @@ -100,7 +101,7 @@ def test_dispute_callback_won(self, save, get_payin, get_dispute): payin = PayIn(tag=str(e_id)) get_payin.return_value = payin # Transfer some of the money to homer - self.janet.set_tip_to(self.homer, D('3.68')) + self.janet.set_tip_to(self.homer, EUR('3.68')) Payday.start().run() # Withdraw some of the money self.make_exchange('mango-ba', D('-2.68'), 0, self.homer) @@ -267,7 +268,7 @@ def test_payin_bank_wire_callback_unexpected(self, Get): ) for status, result_code, error, fee in cases: status_up = status.upper() - homer.set_tip_to(self.janet, D('1.00')) + homer.set_tip_to(self.janet, EUR('1.00')) homer.close('downstream') assert homer.balance == 0 assert homer.status == 'closed' diff --git a/tests/py/test_charts_json.py b/tests/py/test_charts_json.py index 4b67e4d95d..22b41ac602 100644 --- a/tests/py/test_charts_json.py +++ b/tests/py/test_charts_json.py @@ -6,6 +6,7 @@ from pando.utils import utcnow from liberapay.billing.payday import Payday +from liberapay.testing import EUR from liberapay.testing.mangopay import FakeTransfersHarness @@ -25,8 +26,8 @@ def setUp(self): self.make_exchange('mango-cc', 10, 0, self.bob) self.make_participant('notactive') - self.alice.set_tip_to(self.carl, '1.00') - self.bob.set_tip_to(self.carl, '2.00') + self.alice.set_tip_to(self.carl, EUR('1.00')) + self.bob.set_tip_to(self.carl, EUR('2.00')) def run_payday(self): Payday.start().run(recompute_stats=1) @@ -46,8 +47,8 @@ def test_first_payday_comes_through(self): def test_second_payday_comes_through(self): self.run_payday() # first - self.alice.set_tip_to(self.carl, '5.00') - self.bob.set_tip_to(self.carl, '0.00') + self.alice.set_tip_to(self.carl, EUR('5.00')) + self.bob.set_tip_to(self.carl, EUR('0.00')) self.run_payday() # second @@ -63,12 +64,12 @@ def test_sandwiched_tipless_payday_comes_through(self): self.run_payday() # first # Oops! Sorry, Carl. :-( - self.alice.set_tip_to(self.carl, '0.00') - self.bob.set_tip_to(self.carl, '0.00') + self.alice.set_tip_to(self.carl, EUR('0.00')) + self.bob.set_tip_to(self.carl, EUR('0.00')) self.run_payday() # second # Bouncing back ... - self.alice.set_tip_to(self.carl, '5.00') + self.alice.set_tip_to(self.carl, EUR('5.00')) self.run_payday() # third expected = [ @@ -124,8 +125,8 @@ def test_charts_work_for_teams(self): team = self.make_participant('team', kind='group') team.set_take_for(self.bob, 0.1, team) team.set_take_for(self.carl, 1, team) - self.alice.set_tip_to(team, '0.30') - self.bob.set_tip_to(team, '0.59') + self.alice.set_tip_to(team, EUR('0.30')) + self.bob.set_tip_to(team, EUR('0.59')) self.run_payday() diff --git a/tests/py/test_close.py b/tests/py/test_close.py index d36013bada..df4318203c 100644 --- a/tests/py/test_close.py +++ b/tests/py/test_close.py @@ -8,6 +8,7 @@ from liberapay.billing.payday import Payday from liberapay.models.community import Community from liberapay.models.participant import Participant +from liberapay.testing import EUR from liberapay.testing.mangopay import FakeTransfersHarness @@ -21,8 +22,8 @@ def test_close_closes(self): bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - carl.set_tip_to(alice, D('2.00')) + alice.set_tip_to(bob, EUR('3.00')) + carl.set_tip_to(alice, EUR('2.00')) team.add_member(alice) team.add_member(bob) @@ -54,7 +55,7 @@ def test_close_page_is_not_available_during_payday(self): def test_can_post_to_close_page(self): alice = self.make_participant('alice', balance=7) bob = self.make_participant('bob') - alice.set_tip_to(bob, D('10.00')) + alice.set_tip_to(bob, EUR('10.00')) data = {'disburse_to': 'downstream'} response = self.client.PxST('/alice/settings/close', auth_as=alice, data=data) @@ -76,8 +77,8 @@ def test_dbafg_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) + alice.set_tip_to(bob, EUR('3.00')) + alice.set_tip_to(carl, EUR('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('6.00') @@ -88,8 +89,8 @@ def test_dbafg_needs_claimed_tips(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_stub() carl = self.make_stub() - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) + alice.set_tip_to(bob, EUR('3.00')) + alice.set_tip_to(carl, EUR('2.00')) with self.db.get_cursor() as cursor: with pytest.raises(alice.NoOneToGiveFinalGiftTo): alice.distribute_balance_as_final_gift(cursor) @@ -101,8 +102,8 @@ def test_dbafg_gives_all_to_claimed(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_stub() - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) + alice.set_tip_to(bob, EUR('3.00')) + alice.set_tip_to(carl, EUR('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_id(bob.id).balance == D('10.00') @@ -113,8 +114,8 @@ def test_dbafg_skips_zero_tips(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(bob, D('0.00')) - alice.set_tip_to(carl, D('2.00')) + alice.set_tip_to(bob, EUR('0.00')) + alice.set_tip_to(carl, EUR('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips WHERE tippee=%s", (bob.id,)) == 1 @@ -126,8 +127,8 @@ def test_dbafg_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('6.00')) + alice.set_tip_to(bob, EUR('3.00')) + alice.set_tip_to(carl, EUR('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('3.33') @@ -138,8 +139,8 @@ def test_dbafg_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', balance=D('0.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('6.00')) + alice.set_tip_to(bob, EUR('3.00')) + alice.set_tip_to(carl, EUR('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips") == 2 @@ -152,7 +153,7 @@ def test_dbafg_distributes_to_team(self): alice = self.make_participant('alice', balance=D('0.01')) bob = self.make_participant('bob') carl = self.make_participant('carl') - alice.set_tip_to(team, D('3.00')) + alice.set_tip_to(team, EUR('3.00')) team.set_take_for(alice, 1, team) team.set_take_for(bob, 1, team) team.set_take_for(carl, D('0.01'), team) @@ -167,7 +168,7 @@ def test_dbafg_distributes_to_team(self): def test_ctg_clears_tips_giving(self): alice = self.make_participant('alice') - alice.set_tip_to(self.make_participant('bob'), D('1.00')) + alice.set_tip_to(self.make_participant('bob'), EUR('1.00')) ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " "WHERE tipper=%s AND amount > 0", (alice.id,)) @@ -179,8 +180,8 @@ def test_ctg_clears_tips_giving(self): def test_ctg_doesnt_duplicate_zero_tips(self): alice = self.make_participant('alice') bob = self.make_stub() - alice.set_tip_to(bob, D('1.00')) - alice.set_tip_to(bob, D('0.00')) + alice.set_tip_to(bob, EUR('1.00')) + alice.set_tip_to(bob, EUR('0.00')) ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tipper=%s", (alice.id,)) assert ntips() == 2 with self.db.get_cursor() as cursor: @@ -197,11 +198,11 @@ def test_ctg_doesnt_zero_when_theres_no_tip(self): def test_ctg_clears_multiple_tips_giving(self): alice = self.make_participant('alice') - alice.set_tip_to(self.make_participant('bob'), D('1.00')) - alice.set_tip_to(self.make_participant('carl'), D('1.00')) - alice.set_tip_to(self.make_participant('darcy'), D('1.00')) - alice.set_tip_to(self.make_participant('evelyn'), D('1.00')) - alice.set_tip_to(self.make_participant('francis'), D('1.00')) + alice.set_tip_to(self.make_participant('bob'), EUR('1.00')) + alice.set_tip_to(self.make_participant('carl'), EUR('1.00')) + alice.set_tip_to(self.make_participant('darcy'), EUR('1.00')) + alice.set_tip_to(self.make_participant('evelyn'), EUR('1.00')) + alice.set_tip_to(self.make_participant('francis'), EUR('1.00')) ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " "WHERE tipper=%s AND amount > 0", (alice.id,)) @@ -215,7 +216,7 @@ def test_ctg_clears_multiple_tips_giving(self): def test_ctr_clears_tips_receiving(self): alice = self.make_participant('alice') - self.make_participant('bob').set_tip_to(alice, D('1.00')) + self.make_participant('bob').set_tip_to(alice, EUR('1.00')) ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " "WHERE tippee=%s AND amount > 0", (alice.id,)) @@ -227,8 +228,8 @@ def test_ctr_clears_tips_receiving(self): def test_ctr_doesnt_duplicate_zero_tips(self): alice = self.make_participant('alice') bob = self.make_participant('bob') - bob.set_tip_to(alice, D('1.00')) - bob.set_tip_to(alice, D('0.00')) + bob.set_tip_to(alice, EUR('1.00')) + bob.set_tip_to(alice, EUR('0.00')) ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tippee=%s", (alice.id,)) assert ntips() == 2 with self.db.get_cursor() as cursor: @@ -245,11 +246,11 @@ def test_ctr_doesnt_zero_when_theres_no_tip(self): def test_ctr_clears_multiple_tips_receiving(self): alice = self.make_stub() - self.make_participant('bob').set_tip_to(alice, D('1.00')) - self.make_participant('carl').set_tip_to(alice, D('2.00')) - self.make_participant('darcy').set_tip_to(alice, D('3.00')) - self.make_participant('evelyn').set_tip_to(alice, D('4.00')) - self.make_participant('francis').set_tip_to(alice, D('5.00')) + self.make_participant('bob').set_tip_to(alice, EUR('1.00')) + self.make_participant('carl').set_tip_to(alice, EUR('2.00')) + self.make_participant('darcy').set_tip_to(alice, EUR('3.00')) + self.make_participant('evelyn').set_tip_to(alice, EUR('4.00')) + self.make_participant('francis').set_tip_to(alice, EUR('5.00')) ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " "WHERE tippee=%s AND amount > 0", (alice.id,)) diff --git a/tests/py/test_elsewhere.py b/tests/py/test_elsewhere.py index c59efba188..4fb782d9bb 100644 --- a/tests/py/test_elsewhere.py +++ b/tests/py/test_elsewhere.py @@ -7,7 +7,7 @@ from liberapay.elsewhere._base import UserInfo from liberapay.models.account_elsewhere import AccountElsewhere -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness import liberapay.testing.elsewhere as user_info_examples from liberapay.utils import b64encode_s @@ -117,7 +117,7 @@ def test_user_page_shows_pledges(self, get_user_info): alice = self.make_elsewhere('github', 1, 'alice').participant bob = self.make_participant('bob', balance=100) amount = D('14.97') - bob.set_tip_to(alice, amount) + bob.set_tip_to(alice, EUR(amount)) assert alice.receiving == amount r = self.client.GET('/on/github/alice/') assert str(amount) in r.text, r.text diff --git a/tests/py/test_email_notifs.py b/tests/py/test_email_notifs.py index 9c0538e3a3..3c6554c0b8 100644 --- a/tests/py/test_email_notifs.py +++ b/tests/py/test_email_notifs.py @@ -1,4 +1,5 @@ from liberapay.models.participant import Participant +from liberapay.testing import EUR from liberapay.testing.emails import EmailHarness @@ -13,8 +14,8 @@ def setUp(self): def test_take_over_sends_notifications_to_patrons(self): dan_twitter = self.make_elsewhere('twitter', 1, 'dan') - self.alice.set_tip_to(self.dan, '100') # Alice shouldn't receive an email. - self.bob.set_tip_to(dan_twitter, '100') # Bob should receive an email. + self.alice.set_tip_to(self.dan, EUR('100')) # Alice shouldn't receive an email. + self.bob.set_tip_to(dan_twitter, EUR('100')) # Bob should receive an email. self.dan.take_over(dan_twitter, have_confirmation=True) diff --git a/tests/py/test_history.py b/tests/py/test_history.py index b460b18b20..1d39c81c4b 100644 --- a/tests/py/test_history.py +++ b/tests/py/test_history.py @@ -6,7 +6,7 @@ from liberapay.billing.payday import Payday from liberapay.models.participant import Participant -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness from liberapay.testing.mangopay import FakeTransfersHarness from liberapay.utils.history import get_end_of_year_balance, iter_payday_events @@ -53,9 +53,9 @@ def test_iter_payday_events(self): carl = self.make_participant('carl') david = self.make_participant('david') self.make_exchange('mango-cc', 10000, 0, david) - david.set_tip_to(team, Decimal('1.00')) + david.set_tip_to(team, EUR('1.00')) team.set_take_for(bob, Decimal('1.00'), team) - alice.set_tip_to(bob, Decimal('5.00')) + alice.set_tip_to(bob, EUR('5.00')) assert bob.balance == 0 for i in range(2): diff --git a/tests/py/test_pages.py b/tests/py/test_pages.py index 9826e69ace..2444982b11 100644 --- a/tests/py/test_pages.py +++ b/tests/py/test_pages.py @@ -3,7 +3,6 @@ from __future__ import print_function, unicode_literals from collections import OrderedDict -from decimal import Decimal as D import os import re @@ -13,6 +12,7 @@ from liberapay.billing.payday import Payday from liberapay.constants import SESSION +from liberapay.testing import EUR from liberapay.testing.mangopay import MangopayHarness from liberapay.utils import b64encode_s, find_files from liberapay.wireup import NoDB @@ -105,8 +105,8 @@ def test_new_participant_can_browse(self): def test_active_participant_can_browse(self): self.browse_setup() bob = self.make_participant('bob', balance=50) - bob.set_tip_to(self.david, D('1.00')) - self.david.set_tip_to(bob, D('0.50')) + bob.set_tip_to(self.david, EUR('1.00')) + self.david.set_tip_to(bob, EUR('0.50')) self.browse(auth_as=self.david) def test_homepage_in_all_supported_langs(self): @@ -194,7 +194,7 @@ def test_sign_out_doesnt_redirect_xhr(self): def test_giving_page(self): alice = self.make_participant('alice') bob = self.make_participant('bob') - alice.set_tip_to(bob, "1.00") + alice.set_tip_to(bob, EUR('1.00')) actual = self.client.GET("/alice/giving/", auth_as=alice).text expected = "bob" assert expected in actual @@ -202,7 +202,7 @@ def test_giving_page(self): def test_giving_page_shows_pledges(self): alice = self.make_participant('alice') emma = self.make_elsewhere('github', 58946, 'emma').participant - alice.set_tip_to(emma, "1.00") + alice.set_tip_to(emma, EUR('1.00')) actual = self.client.GET("/alice/giving/", auth_as=alice).text expected1 = "emma" expected2 = "Pledges" @@ -212,8 +212,8 @@ def test_giving_page_shows_pledges(self): def test_giving_page_shows_cancelled(self): alice = self.make_participant('alice') bob = self.make_participant('bob') - alice.set_tip_to(bob, "1.00") - alice.set_tip_to(bob, "0.00") + alice.set_tip_to(bob, EUR('1.00')) + alice.set_tip_to(bob, EUR('0.00')) actual = self.client.GET("/alice/giving/", auth_as=alice).text assert "bob" in actual assert "Cancelled" in actual diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py index bb4f132cf4..61d4bf7035 100644 --- a/tests/py/test_participant.py +++ b/tests/py/test_participant.py @@ -20,7 +20,7 @@ UsernameTooLong, ) from liberapay.models.participant import NeedConfirmation, Participant -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness class TestNeedConfirmation(Harness): @@ -50,15 +50,15 @@ def test_empty_stub_is_deleted(self): def test_cross_tip_doesnt_become_self_tip(self): alice = self.make_participant('alice', elsewhere='twitter') bob = self.make_elsewhere('twitter', 2, 'bob') - alice.set_tip_to(bob.participant, '1.00') + alice.set_tip_to(bob.participant, EUR('1.00')) alice.take_over(bob, have_confirmation=True) self.db.self_check() def test_zero_cross_tip_doesnt_become_self_tip(self): alice = self.make_participant('alice') bob = self.make_elsewhere('twitter', 2, 'bob') - alice.set_tip_to(bob.participant, '1.00') - alice.set_tip_to(bob.participant, '0.00') + alice.set_tip_to(bob.participant, EUR('1.00')) + alice.set_tip_to(bob.participant, EUR('0.00')) alice.take_over(bob, have_confirmation=True) self.db.self_check() @@ -66,8 +66,8 @@ def test_do_not_take_over_zero_tips_receiving(self): alice = self.make_participant('alice') bob = self.make_participant('bob') carl = self.make_elsewhere('twitter', 3, 'carl') - bob.set_tip_to(carl, '1.00') - bob.set_tip_to(carl, '0.00') + bob.set_tip_to(carl, EUR('1.00')) + bob.set_tip_to(carl, EUR('0.00')) alice.take_over(carl, have_confirmation=True) ntips = self.db.one("select count(*) from tips") assert 2 == ntips @@ -77,8 +77,8 @@ def test_consolidated_tips_receiving(self): alice = self.make_participant('alice', balance=1) bob = self.make_participant('bob', elsewhere='twitter') carl = self.make_elsewhere('github', -1, 'carl') - alice.set_tip_to(bob, '1.00') # funded - alice.set_tip_to(carl.participant, '5.00') # not funded + alice.set_tip_to(bob, EUR('1.00')) # funded + alice.set_tip_to(carl.participant, EUR('5.00')) # not funded bob.take_over(carl, have_confirmation=True) tips = self.db.all("select * from tips where amount > 0 order by id asc") assert len(tips) == 3 @@ -261,7 +261,7 @@ def test_bad_username(self): def test_stt_sets_tip_to(self): alice = self.make_participant('alice', balance=100) bob = self.make_stub() - alice.set_tip_to(bob, '1.00') + alice.set_tip_to(bob, EUR('1.00')) actual = alice.get_tip_to(bob)['amount'] assert actual == Decimal('1.00') @@ -269,7 +269,7 @@ def test_stt_sets_tip_to(self): def test_stt_works_for_pledges(self): alice = self.make_participant('alice', balance=1) bob = self.make_stub() - t = alice.set_tip_to(bob, '10.00') + t = alice.set_tip_to(bob, EUR('10.00')) assert isinstance(t, dict) assert isinstance(t['amount'], Decimal) assert t['amount'] == 10 @@ -280,7 +280,7 @@ def test_stt_works_for_pledges(self): def test_stt_works_for_donations(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - t = alice.set_tip_to(bob, '1.00') + t = alice.set_tip_to(bob, EUR('1.00')) assert t['amount'] == 1 assert t['is_funded'] is True assert t['is_pledge'] is False @@ -289,38 +289,38 @@ def test_stt_works_for_donations(self): def test_stt_works_for_monthly_donations(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - t = alice.set_tip_to(bob, '4.33', 'monthly') + t = alice.set_tip_to(bob, EUR('4.33'), 'monthly') assert t['amount'] == 1 def test_stt_works_for_yearly_donations(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - t = alice.set_tip_to(bob, '104', 'yearly') + t = alice.set_tip_to(bob, EUR('104'), 'yearly') assert t['amount'] == 2 def test_stt_returns_False_for_second_time_tipper(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '1.00') - actual = alice.set_tip_to(bob, '2.00') + alice.set_tip_to(bob, EUR('1.00')) + actual = alice.set_tip_to(bob, EUR('2.00')) assert actual['amount'] == 2 assert actual['first_time_tipper'] is False def test_stt_doesnt_allow_self_tipping(self): alice = self.make_participant('alice', balance=100) with pytest.raises(NoSelfTipping): - alice.set_tip_to(alice, '10.00') + alice.set_tip_to(alice, EUR('10.00')) def test_stt_doesnt_allow_just_any_ole_amount(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') with pytest.raises(BadAmount): - alice.set_tip_to(bob, '1000.00') + alice.set_tip_to(bob, EUR('1000.00')) def test_stt_fails_to_tip_unknown_people(self): alice = self.make_participant('alice', balance=100) with pytest.raises(NoTippee): - alice.set_tip_to('bob', '1.00') + alice.set_tip_to('bob', EUR('1.00')) # giving, npatrons and receiving @@ -329,11 +329,11 @@ def test_only_funded_tips_count(self): bob = self.make_participant('bob') carl = self.make_participant('carl', last_bill_result="Fail!") dana = self.make_participant('dana') - alice.set_tip_to(dana, '3.00') - alice.set_tip_to(bob, '6.00') - bob.set_tip_to(alice, '5.00') - bob.set_tip_to(dana, '2.00') - carl.set_tip_to(dana, '2.08') + alice.set_tip_to(dana, EUR('3.00')) + alice.set_tip_to(bob, EUR('6.00')) + bob.set_tip_to(alice, EUR('5.00')) + bob.set_tip_to(dana, EUR('2.00')) + carl.set_tip_to(dana, EUR('2.08')) assert alice.giving == Decimal('9.00') assert alice.receiving == Decimal('5.00') @@ -351,10 +351,10 @@ def test_only_latest_tip_counts(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob', balance=100) carl = self.make_participant('carl') - alice.set_tip_to(carl, '12.00') - alice.set_tip_to(carl, '3.00') - bob.set_tip_to(carl, '2.00') - bob.set_tip_to(carl, '0.00') + alice.set_tip_to(carl, EUR('12.00')) + alice.set_tip_to(carl, EUR('3.00')) + bob.set_tip_to(carl, EUR('2.00')) + bob.set_tip_to(carl, EUR('0.00')) assert alice.giving == Decimal('3.00') assert bob.giving == Decimal('0.00') assert carl.receiving == Decimal('3.00') @@ -363,13 +363,13 @@ def test_only_latest_tip_counts(self): def test_receiving_includes_taking_when_updated_from_set_tip_to(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob', taking=Decimal('42.00')) - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) assert Participant.from_username('bob').receiving == bob.receiving == Decimal('45.00') def test_receiving_is_zero_for_patrons(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) bob.update_goal(Decimal('-1')) assert bob.receiving == 0 @@ -383,12 +383,12 @@ def test_cant_pledge_to_locked_accounts(self): alice = self.make_participant('alice', balance=100) bob = self.make_stub(goal=-1) with self.assertRaises(UserDoesntAcceptTips): - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) def test_pledging_isnt_giving(self): alice = self.make_participant('alice', balance=100) bob = self.make_elsewhere('github', 58946, 'bob').participant - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) assert alice.giving == Decimal('0.00') # get_age_in_seconds - gais diff --git a/tests/py/test_payday.py b/tests/py/test_payday.py index a1e158774d..66e96b4724 100644 --- a/tests/py/test_payday.py +++ b/tests/py/test_payday.py @@ -9,7 +9,7 @@ from liberapay.billing.transactions import create_debt from liberapay.exceptions import NegativeBalance from liberapay.models.participant import Participant -from liberapay.testing import Foobar +from liberapay.testing import EUR, Foobar from liberapay.testing.mangopay import FakeTransfersHarness, MangopayHarness from liberapay.testing.emails import EmailHarness @@ -40,7 +40,7 @@ def test_payday_prevents_human_errors(self): @mock.patch.object(Payday, 'transfer_for_real') def test_payday_can_be_restarted_after_crash(self, transfer_for_real, exec_payday): transfer_for_real.side_effect = Foobar - self.janet.set_tip_to(self.homer, '6.00') + self.janet.set_tip_to(self.homer, EUR('6.00')) with self.assertRaises(Foobar): Payday.start().run() # Check that the web interface allows relaunching @@ -60,7 +60,7 @@ def test_payday_id_is_serial(self): assert id == i def test_payday_moves_money(self): - self.janet.set_tip_to(self.homer, '6.00') # under $10! + self.janet.set_tip_to(self.homer, EUR('6.00')) # under $10! self.make_exchange('mango-cc', 10, 0, self.janet) Payday.start().run() @@ -81,17 +81,17 @@ def test_update_cached_amounts(self): emma = Participant.make_stub(username='emma') team2 = self.make_participant('team2', kind='group') team2.add_member(dana) - alice.set_tip_to(dana, '3.00') - alice.set_tip_to(bob, '6.00') - alice.set_tip_to(emma, '0.50') - alice.set_tip_to(team, '1.20') - alice.set_tip_to(team2, '0.49') - bob.set_tip_to(alice, '5.00') + alice.set_tip_to(dana, EUR('3.00')) + alice.set_tip_to(bob, EUR('6.00')) + alice.set_tip_to(emma, EUR('0.50')) + alice.set_tip_to(team, EUR('1.20')) + alice.set_tip_to(team2, EUR('0.49')) + bob.set_tip_to(alice, EUR('5.00')) team.add_member(bob) team.set_take_for(bob, D('1.00'), team) - bob.set_tip_to(dana, '2.00') # funded by bob's take - bob.set_tip_to(emma, '7.00') # not funded, insufficient receiving - carl.set_tip_to(dana, '2.08') # not funded, insufficient balance + bob.set_tip_to(dana, EUR('2.00')) # funded by bob's take + bob.set_tip_to(emma, EUR('7.00')) # not funded, insufficient receiving + carl.set_tip_to(dana, EUR('2.08')) # not funded, insufficient balance def check(): alice = Participant.from_username('alice') @@ -177,7 +177,7 @@ def test_update_cached_amounts_depth(self): prev = alice for user in reversed(users): - prev.set_tip_to(user, '1.00') + prev.set_tip_to(user, EUR('1.00')) prev = user def check(): @@ -275,7 +275,7 @@ def get_new_balances(cursor): def test_payday_doesnt_process_tips_when_goal_is_negative(self): self.make_exchange('mango-cc', 20, 0, self.janet) - self.janet.set_tip_to(self.homer, 13) + self.janet.set_tip_to(self.homer, EUR('13.00')) self.db.run("UPDATE participants SET goal = -1 WHERE username='homer'") payday = Payday.start() with self.db.get_cursor() as cursor: @@ -287,8 +287,8 @@ def test_payday_doesnt_process_tips_when_goal_is_negative(self): def test_payday_doesnt_make_null_transfers(self): alice = self.make_participant('alice') - alice.set_tip_to(self.homer, 1) - alice.set_tip_to(self.homer, 0) + alice.set_tip_to(self.homer, EUR('1.00')) + alice.set_tip_to(self.homer, EUR(0)) a_team = self.make_participant('a_team', kind='group') a_team.add_member(alice) Payday.start().run() @@ -297,8 +297,8 @@ def test_payday_doesnt_make_null_transfers(self): def test_transfer_tips(self): self.make_exchange('mango-cc', 1, 0, self.david) - self.david.set_tip_to(self.janet, D('0.51')) - self.david.set_tip_to(self.homer, D('0.50')) + self.david.set_tip_to(self.janet, EUR('0.51')) + self.david.set_tip_to(self.homer, EUR('0.50')) payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) @@ -312,10 +312,10 @@ def test_transfer_tips(self): def test_transfer_tips_whole_graph(self): alice = self.make_participant('alice', balance=50) - alice.set_tip_to(self.homer, D('50')) - self.homer.set_tip_to(self.janet, D('20')) - self.janet.set_tip_to(self.david, D('5')) - self.david.set_tip_to(self.homer, D('20')) # Should be unfunded + alice.set_tip_to(self.homer, EUR('50')) + self.homer.set_tip_to(self.janet, EUR('20')) + self.janet.set_tip_to(self.david, EUR('5')) + self.david.set_tip_to(self.homer, EUR('20')) # Should be unfunded payday = Payday.start() with self.db.get_cursor() as cursor: @@ -334,7 +334,7 @@ def test_transfer_takes(self): bob = self.make_participant('bob') a_team.set_take_for(bob, D('0.01'), a_team) charlie = self.make_participant('charlie', balance=1000) - charlie.set_tip_to(a_team, D('1.01')) + charlie.set_tip_to(a_team, EUR('1.01')) payday = Payday.start() @@ -370,7 +370,7 @@ def test_underfunded_team(self): bob = self.make_participant('bob') team.set_take_for(bob, D('1.00'), team) charlie = self.make_participant('charlie', balance=1000) - charlie.set_tip_to(team, D('0.26')) + charlie.set_tip_to(team, EUR('0.26')) Payday.start().run() @@ -396,9 +396,9 @@ def test_wellfunded_team(self): bob = self.make_participant('bob') team.set_take_for(bob, D('0.21'), team) charlie = self.make_participant('charlie', balance=10) - charlie.set_tip_to(team, D('5.00')) + charlie.set_tip_to(team, EUR('5.00')) dan = self.make_participant('dan', balance=10) - dan.set_tip_to(team, D('5.00')) + dan.set_tip_to(team, EUR('5.00')) Payday.start().run() @@ -420,7 +420,7 @@ def test_wellfunded_team_with_early_donor(self): bob = self.make_participant('bob') team.set_take_for(bob, D('0.21'), team) charlie = self.make_participant('charlie', balance=10) - charlie.set_tip_to(team, D('2.00')) + charlie.set_tip_to(team, EUR('2.00')) print("> Step 1: three weeks of donations from charlie only") print() @@ -441,7 +441,7 @@ def test_wellfunded_team_with_early_donor(self): "reduced while dan catches up") print() dan = self.make_participant('dan', balance=10) - dan.set_tip_to(team, D('2.00')) + dan.set_tip_to(team, EUR('2.00')) for i in range(6): Payday.start().run(recompute_stats=0, update_cached_amounts=False) @@ -481,9 +481,9 @@ def test_wellfunded_team_with_two_early_donors(self): bob = self.make_participant('bob') team.set_take_for(bob, D('0.21'), team) charlie = self.make_participant('charlie', balance=10) - charlie.set_tip_to(team, D('1.00')) + charlie.set_tip_to(team, EUR('1.00')) dan = self.make_participant('dan', balance=10) - dan.set_tip_to(team, D('3.00')) + dan.set_tip_to(team, EUR('3.00')) print("> Step 1: three weeks of donations from early donors") print() @@ -505,7 +505,7 @@ def test_wellfunded_team_with_two_early_donors(self): "donors automatically decrease while the new donor catches up") print() emma = self.make_participant('emma', balance=10) - emma.set_tip_to(team, D('1.00')) + emma.set_tip_to(team, EUR('1.00')) Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() @@ -561,9 +561,9 @@ def test_wellfunded_team_with_two_early_donors_and_low_amounts(self): bob = self.make_participant('bob') team.set_take_for(bob, D('0.01'), team) charlie = self.make_participant('charlie', balance=10) - charlie.set_tip_to(team, D('0.02')) + charlie.set_tip_to(team, EUR('0.02')) dan = self.make_participant('dan', balance=10) - dan.set_tip_to(team, D('0.02')) + dan.set_tip_to(team, EUR('0.02')) print("> Step 1: three weeks of donations from early donors") print() @@ -585,7 +585,7 @@ def test_wellfunded_team_with_two_early_donors_and_low_amounts(self): "donors automatically decrease while the new donor catches up") print() emma = self.make_participant('emma', balance=10) - emma.set_tip_to(team, D('0.02')) + emma.set_tip_to(team, EUR('0.02')) for i in range(6): Payday.start().run(recompute_stats=0, update_cached_amounts=False) @@ -610,7 +610,7 @@ def test_wellfunded_team_with_early_donor_and_small_leftover(self): bob = self.make_participant('bob') team.set_take_for(bob, D('0.50'), team) charlie = self.make_participant('charlie', balance=10) - charlie.set_tip_to(team, D('0.52')) + charlie.set_tip_to(team, EUR('0.52')) print("> Step 1: three weeks of donations from early donor") print() @@ -632,7 +632,7 @@ def test_wellfunded_team_with_early_donor_and_small_leftover(self): "but the leftover is small so the adjustments are limited") print() dan = self.make_participant('dan', balance=10) - dan.set_tip_to(team, D('0.52')) + dan.set_tip_to(team, EUR('0.52')) for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) @@ -652,10 +652,10 @@ def test_mutual_tipping_through_teams(self): self.clear_tables() team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=8) - alice.set_tip_to(team, D('2.00')) + alice.set_tip_to(team, EUR('2.00')) team.set_take_for(alice, D('0.25'), team) bob = self.make_participant('bob', balance=10) - bob.set_tip_to(team, D('2.00')) + bob.set_tip_to(team, EUR('2.00')) team.set_take_for(bob, D('0.75'), team) Payday.start().run() @@ -672,7 +672,7 @@ def test_unfunded_tip_to_team_doesnt_cause_NegativeBalance(self): self.clear_tables() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') - alice.set_tip_to(team, D('1.00')) # unfunded tip + alice.set_tip_to(team, EUR('1.00')) # unfunded tip bob = self.make_participant('bob') team.set_take_for(bob, D('1.00'), team) @@ -718,7 +718,7 @@ def make_invoice(self, sender, addressee, amount, status): def test_it_handles_invoices_correctly(self): org = self.make_participant('org', kind='organization', allow_invoices=True) self.make_exchange('mango-cc', 60, 0, self.janet) - self.janet.set_tip_to(org, '50.00') + self.janet.set_tip_to(org, EUR('50.00')) self.db.run("UPDATE participants SET allow_invoices = true WHERE id = %s", (self.janet.id,)) self.make_invoice(self.janet, org, '40.02', 'accepted') @@ -764,10 +764,10 @@ def test_payday_tries_to_settle_debts(self): def test_it_notifies_participants(self): self.make_exchange('mango-cc', 10, 0, self.janet) - self.janet.set_tip_to(self.david, '4.50') - self.janet.set_tip_to(self.homer, '3.50') + self.janet.set_tip_to(self.david, EUR('4.50')) + self.janet.set_tip_to(self.homer, EUR('3.50')) team = self.make_participant('team', kind='group', email='team@example.com') - self.janet.set_tip_to(team, '0.25') + self.janet.set_tip_to(team, EUR('0.25')) team.add_member(self.david) team.set_take_for(self.david, D('0.23'), team) self.client.POST('/homer/emails/notifications.json', auth_as=self.homer, @@ -775,7 +775,7 @@ def test_it_notifies_participants(self): kalel = self.make_participant( 'kalel', mangopay_user_id=None, email='kalel@example.org', ) - self.janet.set_tip_to(kalel, '0.10') + self.janet.set_tip_to(kalel, EUR('0.10')) Payday.start().run() david = self.david.refetch() assert david.balance == D('4.73') diff --git a/tests/py/test_payio.py b/tests/py/test_payio.py index 0309d3c212..51014d6c58 100644 --- a/tests/py/test_payio.py +++ b/tests/py/test_payio.py @@ -30,7 +30,7 @@ ) from liberapay.models.exchange_route import ExchangeRoute from liberapay.models.participant import Participant -from liberapay.testing import Foobar +from liberapay.testing import EUR, Foobar from liberapay.testing.mangopay import FakeTransfersHarness, MangopayHarness @@ -48,7 +48,7 @@ class TestPayouts(MangopayHarness): def test_payout(self): e = charge(self.db, self.janet_route, D('46.00'), 'http://localhost/') assert e.status == 'succeeded', e.note - self.janet.set_tip_to(self.homer, '42.00') + self.janet.set_tip_to(self.homer, EUR('42.00')) self.janet.close('downstream') self.homer = self.homer.refetch() assert self.homer.balance == 46 @@ -196,7 +196,7 @@ def test_payin_bank_wire_creation(self): r = self.client.PxST(path, data, auth_as=self.janet) assert r.code == 403 # rejected because janet has no donations set up - self.janet.set_tip_to(self.david, '10.00') + self.janet.set_tip_to(self.david, EUR('10.00')) r = self.client.PxST(path, data, auth_as=self.janet) assert r.code == 302, r.text redir = r.headers[b'Location'] @@ -235,7 +235,7 @@ class TestDirectDebit(MangopayHarness): def test_direct_debit_form(self): path = b'/janet/wallet/payin/direct-debit' - self.janet.set_tip_to(self.david, '10.00') + self.janet.set_tip_to(self.david, EUR('10.00')) r = self.client.GET(path, auth_as=self.janet) assert r.code == 200 @@ -249,7 +249,7 @@ def test_direct_debit_creation(self, url): r = self.client.PxST(path, data, auth_as=self.homer) assert r.code == 403 # rejected because homer has no donations set up - self.homer.set_tip_to(self.david, '10.00') + self.homer.set_tip_to(self.david, EUR('10.00')) r = self.client.GET(path, auth_as=self.homer) assert b'FRxxxxxxxxxxxxxxxxxxxxx2606' in r.body, r.text diff --git a/tests/py/test_public_json.py b/tests/py/test_public_json.py index 7d8f454ad9..1069a6b9f5 100644 --- a/tests/py/test_public_json.py +++ b/tests/py/test_public_json.py @@ -2,7 +2,7 @@ import json -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness class Tests(Harness): @@ -11,7 +11,7 @@ def test_anonymous(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '1.00') + alice.set_tip_to(bob, EUR('1.00')) data = json.loads(self.client.GET('/bob/public.json').text) assert data['receiving'] == '1.00' @@ -23,7 +23,7 @@ def test_anonymous(self): def test_anonymous_gets_null_giving_if_user_anonymous(self): alice = self.make_participant('alice', balance=100, hide_giving=True) bob = self.make_participant('bob') - alice.set_tip_to(bob, '1.00') + alice.set_tip_to(bob, EUR('1.00')) data = json.loads(self.client.GET('/alice/public.json').text) assert data['giving'] == None @@ -31,7 +31,7 @@ def test_anonymous_gets_null_giving_if_user_anonymous(self): def test_anonymous_gets_null_receiving_if_user_anonymous(self): alice = self.make_participant('alice', balance=100, hide_receiving=True) bob = self.make_participant('bob') - alice.set_tip_to(bob, '1.00') + alice.set_tip_to(bob, EUR('1.00')) data = json.loads(self.client.GET('/alice/public.json').text) assert data['receiving'] == None @@ -55,7 +55,7 @@ def test_authenticated_user_gets_their_tip(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '1.00') + alice.set_tip_to(bob, EUR('1.00')) raw = self.client.GET('/bob/public.json', auth_as=alice).text @@ -70,9 +70,9 @@ def test_authenticated_user_doesnt_get_other_peoples_tips(self): carl = self.make_participant('carl', balance=100) dana = self.make_participant('dana') - alice.set_tip_to(dana, '1.00') - bob.set_tip_to(dana, '3.00') - carl.set_tip_to(dana, '12.00') + alice.set_tip_to(dana, EUR('1.00')) + bob.set_tip_to(dana, EUR('3.00')) + carl.set_tip_to(dana, EUR('12.00')) raw = self.client.GET('/dana/public.json', auth_as=alice).text @@ -86,7 +86,7 @@ def test_authenticated_user_gets_zero_if_they_dont_tip(self): bob = self.make_participant('bob', balance=100) carl = self.make_participant('carl') - bob.set_tip_to(carl, '3.00') + bob.set_tip_to(carl, EUR('3.00')) raw = self.client.GET('/carl/public.json', auth_as=alice).text @@ -99,7 +99,7 @@ def test_authenticated_user_gets_self_for_self(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) raw = self.client.GET('/bob/public.json', auth_as=bob).text @@ -118,7 +118,7 @@ def test_jsonp_works(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') - alice.set_tip_to(bob, '3.00') + alice.set_tip_to(bob, EUR('3.00')) raw = self.client.GET('/bob/public.json?callback=foo', auth_as=bob).text diff --git a/tests/py/test_stats.py b/tests/py/test_stats.py index 39bc6b20a5..46e28836eb 100644 --- a/tests/py/test_stats.py +++ b/tests/py/test_stats.py @@ -3,7 +3,7 @@ from decimal import Decimal import json -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness class TestChartOfReceiving(Harness): @@ -15,7 +15,7 @@ def setUp(self): setattr(self, participant, p) def test_get_tip_distribution_handles_a_tip(self): - self.alice.set_tip_to(self.bob, '3.00') + self.alice.set_tip_to(self.bob, EUR('3.00')) expected = ([[Decimal('3.00'), 1, Decimal('3.00'), 1.0, Decimal('1')]], 1.0, Decimal('3.00')) actual = self.bob.get_tip_distribution() @@ -28,8 +28,8 @@ def test_get_tip_distribution_handles_no_tips(self): def test_get_tip_distribution_handles_multiple_tips(self): carl = self.make_participant('carl', balance=100) - self.alice.set_tip_to(self.bob, '1.00') - carl.set_tip_to(self.bob, '3.00') + self.alice.set_tip_to(self.bob, EUR('1.00')) + carl.set_tip_to(self.bob, EUR('3.00')) expected = ([ [Decimal('1.00'), 1, Decimal('1.00'), 0.5, Decimal('0.25')], [Decimal('3.00'), 1, Decimal('3.00'), 0.5, Decimal('0.75')] @@ -39,8 +39,8 @@ def test_get_tip_distribution_handles_multiple_tips(self): def test_get_tip_distribution_ignores_bad_cc(self): bad_cc = self.make_participant('bad_cc', last_bill_result='Failure!') - self.alice.set_tip_to(self.bob, '1.00') - bad_cc.set_tip_to(self.bob, '3.00') + self.alice.set_tip_to(self.bob, EUR('1.00')) + bad_cc.set_tip_to(self.bob, EUR('3.00')) expected = ([[Decimal('1.00'), 1, Decimal('1.00'), 1, Decimal('1')]], 1.0, Decimal('1.00')) actual = self.bob.get_tip_distribution() @@ -48,8 +48,8 @@ def test_get_tip_distribution_ignores_bad_cc(self): def test_get_tip_distribution_ignores_missing_cc(self): missing_cc = self.make_participant('missing_cc') - self.alice.set_tip_to(self.bob, '1.00') - missing_cc.set_tip_to(self.bob, '3.00') + self.alice.set_tip_to(self.bob, EUR('1.00')) + missing_cc.set_tip_to(self.bob, EUR('3.00')) expected = ([[Decimal('1.00'), 1, Decimal('1.00'), 1, Decimal('1')]], 1.0, Decimal('1.00')) actual = self.bob.get_tip_distribution() diff --git a/tests/py/test_take.py b/tests/py/test_take.py index 128178b1cb..efe7fcebeb 100644 --- a/tests/py/test_take.py +++ b/tests/py/test_take.py @@ -5,7 +5,7 @@ from psycopg2 import InternalError from liberapay.billing.payday import Payday -from liberapay.testing import Harness +from liberapay.testing import EUR, Harness from liberapay.models.participant import Participant @@ -18,7 +18,7 @@ def make_team(self, username=TEAM, **kw): team = self.make_participant(username, kind='group', **kw) if Participant.from_username('Daddy Warbucks') is None: self.warbucks = self.make_participant('Daddy Warbucks', balance=1000) - self.warbucks.set_tip_to(team, '100') + self.warbucks.set_tip_to(team, EUR('100')) return team def make_team_of_one(self, username=TEAM, **kw): @@ -70,21 +70,21 @@ def test_take_can_double(self): def test_take_can_double_but_not_a_penny_more(self): team, alice, bob = self.make_team_of_two() - self.warbucks.set_tip_to(team, '20') + self.warbucks.set_tip_to(team, EUR('20')) self.take_last_week(team, alice, '40.00') actual = team.set_take_for(alice, D('80.01'), alice) assert actual == 80 def test_increase_is_based_on_nominal_take_last_week(self): team, alice, bob = self.make_team_of_two() - self.warbucks.set_tip_to(team, '15.03') + self.warbucks.set_tip_to(team, EUR('15.03')) self.take_last_week(team, alice, '20.00', actual_amount='15.03') team.set_take_for(alice, D('35.00'), team, check_max=False) assert team.set_take_for(alice, D('42.00'), alice) == 40 def test_if_last_week_is_less_than_one_can_increase_to_one(self): team, alice, bob = self.make_team_of_two() - self.warbucks.set_tip_to(team, '0.50') + self.warbucks.set_tip_to(team, EUR('0.50')) self.take_last_week(team, alice, '0.01') actual = team.set_take_for(alice, D('42.00'), team) assert actual == 1 @@ -124,14 +124,14 @@ def test_get_members(self): def test_taking_and_receiving_are_updated_correctly(self): team, alice = self.make_team_of_one() self.take_last_week(team, alice, '40.00') - self.warbucks.set_tip_to(team, D('42.00')) + self.warbucks.set_tip_to(team, EUR('42.00')) team.set_take_for(alice, D('42.00'), alice) assert alice.taking == 42 assert alice.receiving == 42 - self.warbucks.set_tip_to(alice, D('10.00')) + self.warbucks.set_tip_to(alice, EUR('10.00')) assert alice.taking == 42 assert alice.receiving == 52 - self.warbucks.set_tip_to(team, D('50.00')) + self.warbucks.set_tip_to(team, EUR('50.00')) assert team.receiving == 50 team.set_take_for(alice, D('50.00'), alice) assert alice.taking == 50 @@ -155,7 +155,7 @@ def test_changes_to_team_receiving_affect_members_take(self): self.take_last_week(team, alice, '40.00') team.set_take_for(alice, D('42.00'), alice) - self.warbucks.set_tip_to(team, D('10.00')) # hard times + self.warbucks.set_tip_to(team, EUR('10.00')) # hard times alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 10 From c72bc3ed573edadec2fe88e4a3bd5d620866c68e Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 15 Oct 2017 13:11:24 +0200 Subject: [PATCH 5/7] adapt fees to the currency --- liberapay/billing/fees.py | 40 +++++++++------- liberapay/billing/transactions.py | 41 +++++++++------- liberapay/constants.py | 48 ++++++++++++++----- tests/py/test_payio.py | 34 ++++++------- www/%username/wallet/payin/%back_to.spt | 20 ++++---- .../wallet/payin/bankwire/%back_to.spt | 12 ++--- www/%username/wallet/payin/card/%back_to.spt | 12 ++--- .../payin/direct-debit/%exchange_id.spt | 11 ++--- www/%username/wallet/payout/%back_to.spt | 8 ++-- www/about/faq.spt | 43 ++++++++++------- www/callbacks/mangopay.spt | 23 +++++---- 11 files changed, 168 insertions(+), 124 deletions(-) diff --git a/liberapay/billing/fees.py b/liberapay/billing/fees.py index 7355e8cae5..e3faf1e61d 100644 --- a/liberapay/billing/fees.py +++ b/liberapay/billing/fees.py @@ -2,24 +2,23 @@ """ from __future__ import division, print_function, unicode_literals -from decimal import Decimal, ROUND_UP - -from pando.utils import typecheck +from mangopay.utils import Money from liberapay.constants import ( - D_CENT, PAYIN_CARD_MIN, FEE_PAYIN_CARD, FEE_PAYIN_BANK_WIRE, PAYIN_BANK_WIRE_MIN, FEE_PAYIN_DIRECT_DEBIT, PAYIN_DIRECT_DEBIT_MIN, - FEE_PAYOUT, FEE_PAYOUT_OUTSIDE_SEPA, SEPA, - FEE_VAT, + FEE_PAYOUT, + Fees, ) def upcharge(amount, fees, min_amount): """Given an amount, return a higher amount and the difference. """ - typecheck(amount, Decimal) + assert isinstance(amount, Money), type(amount) + + fees = fees if isinstance(fees, Fees) else fees[amount.currency] if amount < min_amount: amount = min_amount @@ -30,14 +29,14 @@ def upcharge(amount, fees, min_amount): fee = charge_amount - amount # + VAT - vat = fee * FEE_VAT + vat = fee * Fees.VAT charge_amount += vat fee += vat # Round - charge_amount = charge_amount.quantize(D_CENT, rounding=ROUND_UP) - fee = fee.quantize(D_CENT, rounding=ROUND_UP) - vat = vat.quantize(D_CENT, rounding=ROUND_UP) + charge_amount = charge_amount.round_up() + fee = fee.round_up() + vat = vat.round_up() return charge_amount, fee, vat @@ -51,10 +50,10 @@ def skim_amount(amount, fees): """Given a nominal amount, compute the fees, taxes, and the actual amount. """ fee = amount * fees.var + fees.fix - vat = fee * FEE_VAT + vat = fee * Fees.VAT fee += vat - fee = fee.quantize(D_CENT, rounding=ROUND_UP) - vat = vat.quantize(D_CENT, rounding=ROUND_UP) + fee = fee.round_up() + vat = vat.round_up() return amount - fee, fee, vat @@ -76,10 +75,15 @@ def skim_credit(amount, ba): The returned amount can be negative, look out for that. """ - typecheck(amount, Decimal) + assert isinstance(amount, Money), type(amount) + fees = FEE_PAYOUT[amount.currency] country = get_bank_account_country(ba) - if country in SEPA: - fee = FEE_PAYOUT + if 'domestic' in fees: + countries, domestic_fee = fees['domestic'] + if country in countries: + fee = domestic_fee + else: + fee = fees['foreign'] else: - fee = FEE_PAYOUT_OUTSIDE_SEPA + fee = fees['*'] return skim_amount(amount, fee) diff --git a/liberapay/billing/transactions.py b/liberapay/billing/transactions.py index 472ac5646a..5d8aad6d6e 100644 --- a/liberapay/billing/transactions.py +++ b/liberapay/billing/transactions.py @@ -10,7 +10,6 @@ SettlementTransfer, Transfer, User, Wallet, ) from mangopay.utils import Money -from pando.utils import typecheck from liberapay.billing.fees import ( skim_bank_wire, skim_credit, upcharge_card, upcharge_direct_debit @@ -84,6 +83,7 @@ def payout(db, route, amount, ignore_high_fee=False): ba = BankAccount.get(route.address, user_id=participant.mangopay_user_id) # Do final calculations + amount = Money(amount, 'EUR') if isinstance(amount, Decimal) else amount credit_amount, fee, vat = skim_credit(amount, ba) if credit_amount <= 0 and fee > 0: raise FeeExceedsAmount @@ -95,9 +95,9 @@ def payout(db, route, amount, ignore_high_fee=False): e_id = record_exchange(db, route, -credit_amount, fee, vat, participant, 'pre').id payout = BankWirePayOut() payout.AuthorId = participant.mangopay_user_id - payout.DebitedFunds = Money(int(amount * 100), 'EUR') + payout.DebitedFunds = amount.int() payout.DebitedWalletId = participant.mangopay_wallet_id - payout.Fees = Money(int(fee * 100), 'EUR') + payout.Fees = fee.int() payout.BankAccountId = route.address payout.BankWireRef = str(e_id) payout.Tag = str(e_id) @@ -119,12 +119,13 @@ def charge(db, route, amount, return_url): and add it to amount to end up with charge_amount. """ - typecheck(amount, Decimal) + assert isinstance(amount, (Decimal, Money)), type(amount) assert route assert route.network == 'mango-cc' participant = route.participant + amount = Money(amount, 'EUR') if isinstance(amount, Decimal) else amount charge_amount, fee, vat = upcharge_card(amount) amount = charge_amount - fee @@ -135,10 +136,10 @@ def charge(db, route, amount, return_url): payin = DirectPayIn() payin.AuthorId = participant.mangopay_user_id payin.CreditedWalletId = participant.mangopay_wallet_id - payin.DebitedFunds = Money(int(charge_amount * 100), 'EUR') + payin.DebitedFunds = charge_amount.int() payin.CardId = route.address payin.SecureModeReturnURL = return_url - payin.Fees = Money(int(fee * 100), 'EUR') + payin.Fees = fee.int() payin.Tag = str(e_id) try: test_hook() @@ -158,12 +159,13 @@ def charge(db, route, amount, return_url): def prepare_direct_debit(db, route, amount): """Prepare to debit a bank account. """ - typecheck(amount, Decimal) + assert isinstance(amount, (Decimal, Money)), type(amount) assert route.network == 'mango-ba' participant = route.participant + amount = Money(amount, 'EUR') if isinstance(amount, Decimal) else amount debit_amount, fee, vat = upcharge_direct_debit(amount) amount = debit_amount - fee @@ -197,16 +199,16 @@ def execute_direct_debit(db, exchange, route): assert exchange.status == 'pre' - amount, fee = exchange.amount, exchange.fee + amount, fee = Money(exchange.amount, 'EUR'), Money(exchange.fee, 'EUR') debit_amount = amount + fee e_id = exchange.id payin = DirectDebitDirectPayIn() payin.AuthorId = participant.mangopay_user_id payin.CreditedWalletId = participant.mangopay_wallet_id - payin.DebitedFunds = Money(int(debit_amount * 100), 'EUR') + payin.DebitedFunds = debit_amount.int() payin.MandateId = route.mandate - payin.Fees = Money(int(fee * 100), 'EUR') + payin.Fees = fee.int() payin.Tag = str(e_id) try: test_hook() @@ -229,6 +231,8 @@ def payin_bank_wire(db, participant, debit_amount): route = ExchangeRoute.upsert_bankwire_route(participant) + if not isinstance(debit_amount, Money): + debit_amount = Money(debit_amount, 'EUR') amount, fee, vat = skim_bank_wire(debit_amount) if not participant.mangopay_wallet_id: @@ -238,8 +242,8 @@ def payin_bank_wire(db, participant, debit_amount): payin = BankWirePayIn() payin.AuthorId = participant.mangopay_user_id payin.CreditedWalletId = participant.mangopay_wallet_id - payin.DeclaredDebitedFunds = Money(int(debit_amount * 100), 'EUR') - payin.DeclaredFees = Money(int(fee * 100), 'EUR') + payin.DeclaredDebitedFunds = debit_amount.int() + payin.DeclaredFees = fee.int() payin.Tag = str(e_id) try: test_hook() @@ -258,19 +262,20 @@ def record_unexpected_payin(db, payin): """Record an unexpected bank wire payin. """ assert payin.PaymentType == 'BANK_WIRE' - amount = Decimal(payin.DebitedFunds.Amount) / Decimal(100) - paid_fee = Decimal(payin.Fees.Amount) / Decimal(100) - vat = skim_bank_wire(amount)[2] + debited_amount = payin.DebitedFunds / Decimal(100) + paid_fee = payin.Fees / Decimal(100) + vat = skim_bank_wire(debited_amount)[2].amount wallet_id = payin.CreditedWalletId participant = Participant.from_mangopay_user_id(payin.AuthorId) assert participant.mangopay_wallet_id == wallet_id route = ExchangeRoute.upsert_bankwire_route(participant) + amount = (debited_amount - paid_fee).amount return db.one(""" INSERT INTO exchanges (amount, fee, vat, participant, status, route, note, remote_id, wallet_id) VALUES (%s, %s, %s, %s, 'created', %s, NULL, %s, %s) RETURNING id - """, (amount, paid_fee, vat, participant.id, route.id, payin.Id, wallet_id)) + """, (amount, paid_fee.amount, vat, participant.id, route.id, payin.Id, wallet_id)) def record_payout_refund(db, payout_refund): @@ -314,6 +319,10 @@ def record_exchange(db, route, amount, fee, vat, participant, status, error=None if participant.is_suspended: raise AccountSuspended() + amount = getattr(amount, 'amount', amount) + fee = getattr(fee, 'amount', fee) + vat = getattr(vat, 'amount', vat) + with db.get_cursor() as cursor: wallet_id = participant.mangopay_wallet_id diff --git a/liberapay/constants.py b/liberapay/constants.py index 7ed5980dae..9d5caf297e 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -31,7 +31,16 @@ def check_bits(bits): Event = namedtuple('Event', 'name bit title') -Fees = namedtuple('Fees', ('var', 'fix')) + +class Fees(namedtuple('Fees', ('var', 'fix'))): + VAT = Decimal('0.17') # 17% (Luxembourg rate) + VAT_1 = VAT + 1 + + @property + def with_vat(self): + r = (self.var * self.VAT_1 * 100, self.fix * self.VAT_1) + return r[0] if not r[1] else r[1] if not r[0] else r + StandardTip = namedtuple('StandardTip', 'label weekly monthly yearly') @@ -109,13 +118,33 @@ def check_bits(bits): EVENTS_S = ' '.join(EVENTS.keys()) # https://www.mangopay.com/pricing/ -FEE_PAYIN_BANK_WIRE = Fees(Decimal('0.005'), Decimal(0)) # 0.5% -FEE_PAYIN_CARD = Fees(Decimal('0.018'), Decimal('0.18')) # 1.8% + €0.18 -FEE_PAYIN_DIRECT_DEBIT = Fees(Decimal(0), Decimal('0.80')) # €0.80 -FEE_PAYOUT = Fees(Decimal(0), Decimal(0)) -FEE_PAYOUT_OUTSIDE_SEPA = Fees(Decimal(0), Decimal('2.5')) +SEPA = set(""" + AT BE BG CH CY CZ DE DK EE ES ES FI FR GB GI GR HR HU IE IS IT LI LT LU LV + MC MT NL NO PL PT RO SE SI SK +""".split()) +FEE_PAYIN_BANK_WIRE = Fees(Decimal('0.005'), 0) # 0.5% +FEE_PAYIN_CARD = { + 'EUR': Fees(Decimal('0.018'), Money('0.18', 'EUR')), # 1.8% + €0.18 + 'USD': Fees(Decimal('0.025'), Money('0.30', 'USD')), # 2.5% + $0.30 +} +FEE_PAYIN_DIRECT_DEBIT = { + 'EUR': Fees(0, Money('0.80', 'EUR')), # €0.80 + 'GBP': Fees(0, Money('0.80', 'GBP')), # £0.80 +} +FEE_PAYOUT = { + 'EUR': { + 'domestic': (SEPA, Fees(0, 0)), + 'foreign': Fees(0, Money('2.50', 'EUR')), + }, + 'GBP': { + 'domestic': ({'GB'}, Fees(0, Money('0.45', 'GBP'))), + 'foreign': Fees(0, Money('1.90', 'GBP')), + }, + 'USD': { + '*': Fees(0, Money('3.00', 'USD')), + }, +} FEE_PAYOUT_WARN = Decimal('0.03') # warn user when fee exceeds 3% -FEE_VAT = Decimal('0.17') # 17% (Luxembourg rate) INVOICE_DOC_MAX_SIZE = 5000000 INVOICE_DOCS_EXTS = ['pdf', 'jpeg', 'jpg', 'png'] @@ -211,11 +240,6 @@ def check_bits(bits): 'sign-up.ip-version': (15, 15*60), # 15 per 15 minutes per IP version } -SEPA = set(""" - AT BE BG CH CY CZ DE DK EE ES ES FI FR GB GI GR HR HU IE IS IT LI LT LU LV - MC MT NL NO PL PT RO SE SI SK -""".split()) - SESSION = str('session') # bytes in python2, unicode in python3 SESSION_REFRESH = timedelta(hours=1) SESSION_TIMEOUT = timedelta(hours=6) diff --git a/tests/py/test_payio.py b/tests/py/test_payio.py index 51014d6c58..92ca37b6de 100644 --- a/tests/py/test_payio.py +++ b/tests/py/test_payio.py @@ -191,7 +191,7 @@ class TestPayinBankWire(MangopayHarness): def test_payin_bank_wire_creation(self): path = b'/janet/wallet/payin/bankwire/' - data = {'amount': str(upcharge_bank_wire(D('10.00'))[0])} + data = {'amount': str(upcharge_bank_wire(EUR('10.00'))[0].amount)} r = self.client.PxST(path, data, auth_as=self.janet) assert r.code == 403 # rejected because janet has no donations set up @@ -301,44 +301,44 @@ def test_direct_debit_failure(self, save): class TestFees(MangopayHarness): def test_upcharge_basically_works(self): - actual = upcharge_card(D('20.00')) - expected = (D('20.65'), D('0.65'), D('0.10')) + actual = upcharge_card(EUR('20.00')) + expected = (EUR('20.65'), EUR('0.65'), EUR('0.10')) assert actual == expected def test_upcharge_full_in_rounded_case(self): - actual = upcharge_card(D('5.00')) - expected = upcharge_card(PAYIN_CARD_MIN) + actual = upcharge_card(EUR('5.00')) + expected = upcharge_card(EUR(PAYIN_CARD_MIN)) assert actual == expected def test_upcharge_at_min(self): - actual = upcharge_card(PAYIN_CARD_MIN) - expected = (D('15.54'), D('0.54'), D('0.08')) + actual = upcharge_card(EUR(PAYIN_CARD_MIN)) + expected = (EUR('15.54'), EUR('0.54'), EUR('0.08')) assert actual == expected assert actual[1] / actual[0] < D('0.035') # less than 3.5% fee def test_upcharge_at_target(self): - actual = upcharge_card(PAYIN_CARD_TARGET) - expected = (D('94.19'), D('2.19'), D('0.32')) + actual = upcharge_card(EUR(PAYIN_CARD_TARGET)) + expected = (EUR('94.19'), EUR('2.19'), EUR('0.32')) assert actual == expected assert actual[1] / actual[0] < D('0.024') # less than 2.4% fee def test_upcharge_at_one_cent(self): - actual = upcharge_card(D('0.01')) - expected = upcharge_card(PAYIN_CARD_MIN) + actual = upcharge_card(EUR('0.01')) + expected = upcharge_card(EUR(PAYIN_CARD_MIN)) assert actual == expected def test_upcharge_at_min_minus_one_cent(self): - actual = upcharge_card(PAYIN_CARD_MIN - D('0.01')) - expected = upcharge_card(PAYIN_CARD_MIN) + actual = upcharge_card(EUR(PAYIN_CARD_MIN) - EUR('0.01')) + expected = upcharge_card(EUR(PAYIN_CARD_MIN)) assert actual == expected def test_skim_credit(self): - actual = skim_credit(D('10.00'), self.bank_account) - assert actual == (D('10.00'), D('0.00'), D('0.00')) + actual = skim_credit(EUR('10.00'), self.bank_account) + assert actual == (EUR('10.00'), EUR('0.00'), EUR('0.00')) def test_skim_credit_outside_sepa(self): - actual = skim_credit(D('10.00'), self.bank_account_outside_sepa) - assert actual == (D('7.07'), D('2.93'), D('0.43')) + actual = skim_credit(EUR('10.00'), self.bank_account_outside_sepa) + assert actual == (EUR('7.07'), EUR('2.93'), EUR('0.43')) class TestRecordExchange(MangopayHarness): diff --git a/www/%username/wallet/payin/%back_to.spt b/www/%username/wallet/payin/%back_to.spt index 8c385196bf..122873eb84 100644 --- a/www/%username/wallet/payin/%back_to.spt +++ b/www/%username/wallet/payin/%back_to.spt @@ -65,6 +65,7 @@ title = _("Adding Money")

    % from "templates/icons.html" import fontawesome % set base_path = participant.path('wallet/payin') + % set direct_debit_available = currency in constants.FEE_PAYIN_DIRECT_DEBIT
    @@ -72,10 +73,7 @@ title = _("Adding Money")

    {{ _("Credit Card") }}
    Visa / Mastercard / CB

    {{ _("Easy and instantaneous") }}
    - {{ _("Fees: {0}% + {1}", - constants.FEE_PAYIN_CARD.var * (constants.FEE_VAT + 1) * 100, - Money(constants.FEE_PAYIN_CARD.fix * (constants.FEE_VAT + 1), 'EUR'), - ) }} + {{ _("Fees: {0}% + {1}", *constants.FEE_PAYIN_CARD[currency].with_vat) }}

    @@ -85,29 +83,29 @@ title = _("Adding Money")

    {{ fontawesome('bank') }}

    {{ _("Bank Wire") }}
    - {{ _("To a Euro account") }}

    + {{ _("To an account in Europe") }}

    - {{ _("Cheapest for small amounts¹") }}
    - {{ _("Fee: {0}%", - constants.FEE_PAYIN_BANK_WIRE.var * (constants.FEE_VAT + 1) * 100) }} + {{ _("Cheapest for small amounts¹") if direct_debit_available else + _("Cheaper¹ but cumbersome") }}
    + {{ _("Fee: {0}%", constants.FEE_PAYIN_BANK_WIRE.with_vat) }}

    + % if direct_debit_available

    {{ fontawesome('exchange') }}

    {{ _("Direct Debit") }}
    {{ _("SEPA only") }}

    {{ _("Best for regular payments") }}
    - {{ _("Fee: {0}", - Money(constants.FEE_PAYIN_DIRECT_DEBIT.fix * (constants.FEE_VAT + 1), 'EUR'), - ) }} + {{ _("Fee: {0}", constants.FEE_PAYIN_DIRECT_DEBIT[currency].with_vat) }}

    + % endif
    diff --git a/www/%username/wallet/payin/bankwire/%back_to.spt b/www/%username/wallet/payin/bankwire/%back_to.spt index 516dd5681b..4dd5d2a6af 100644 --- a/www/%username/wallet/payin/bankwire/%back_to.spt +++ b/www/%username/wallet/payin/bankwire/%back_to.spt @@ -12,8 +12,8 @@ from liberapay.exceptions import InvalidNumber from liberapay.utils import b64decode_s, get_participant from liberapay.utils.i18n import Money -AMOUNT_MIN = Money(upcharge_bank_wire(PAYIN_BANK_WIRE_MIN)[0], 'EUR') -AMOUNT_MAX = Money(upcharge_bank_wire(KYC_PAYIN_YEARLY_THRESHOLD)[0], 'EUR') +AMOUNT_MIN = upcharge_bank_wire(Money(PAYIN_BANK_WIRE_MIN, 'EUR'))[0] +AMOUNT_MAX = upcharge_bank_wire(Money(KYC_PAYIN_YEARLY_THRESHOLD, 'EUR'))[0] NOTIF_BIT_FAIL = EVENTS['payin_bankwire_failed'].bit NOTIF_BIT_SUCC = EVENTS['payin_bankwire_succeeded'].bit @@ -202,7 +202,7 @@ title = _("Adding Money")

    {{ _( "Adding money to Liberapay via bank wire incurs a fee of {0}% from our " "payment processor.", - constants.FEE_PAYIN_BANK_WIRE.var * (constants.FEE_VAT + 1) * 100, + constants.FEE_PAYIN_BANK_WIRE.with_vat, ) }}

    {{ _("Amount") }}

    @@ -213,16 +213,16 @@ title = _("Adding Money")
      % for weeks in weeks_list % set amount = weekly * weeks - % set charge_amount, fees, vat = upcharge_bank_wire(amount) + % set charge_amount, fees, vat = upcharge_bank_wire(Money(amount, 'EUR')) % set _months = weeks / D('4.33') % set months = _months.quantize(D('1'))