Skip to content

Commit

Permalink
adapt payin constants to the currency
Browse files Browse the repository at this point in the history
  • Loading branch information
Changaco committed Oct 29, 2017
1 parent d945bca commit f506134
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 42 deletions.
3 changes: 2 additions & 1 deletion liberapay/billing/fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
)


def upcharge(amount, fees, min_amount):
def upcharge(amount, fees, min_amounts):
"""Given an amount, return a higher amount and the difference.
"""
assert isinstance(amount, Money), type(amount)

fees = fees if isinstance(fees, Fees) else fees[amount.currency]

min_amount = min_amounts[amount.currency]
if amount < min_amount:
amount = min_amount

Expand Down
29 changes: 23 additions & 6 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,29 @@ def with_vat(self):
PASSWORD_MIN_SIZE = 8
PASSWORD_MAX_SIZE = 150

PAYIN_BANK_WIRE_MIN = Decimal('2.00') # fee ≈ 0.99%
PAYIN_BANK_WIRE_TARGET = Decimal('5.00') # fee ≈ 0.6%
PAYIN_CARD_MIN = Decimal("15.00") # fee ≈ 3.5%
PAYIN_CARD_TARGET = Decimal("92.00") # fee ≈ 2.33%
PAYIN_DIRECT_DEBIT_MIN = Decimal('25.00') # fee ≈ 3.6%
PAYIN_DIRECT_DEBIT_TARGET = Decimal('99.00') # fee ≈ 0.94%
PAYIN_BANK_WIRE_MIN = {k: Money('2.00', k) for k in ('EUR', 'USD')} # fee ≈ 0.99%
PAYIN_BANK_WIRE_TARGET = {k: Money('5.00', k) for k in ('EUR', 'USD')} # fee ≈ 0.6%
PAYIN_BANK_WIRE_MAX = {k: Money('2500.00', k) for k in ('EUR', 'USD')}
PAYIN_CARD_MIN = {
'EUR': Money('15.00', 'EUR'), # fee ≈ 3.5%
'USD': Money('20.00', 'USD'), # fee ≈ 4.58%
}
PAYIN_CARD_TARGET = {
'EUR': Money('92.00', 'EUR'), # fee ≈ 2.33%
'USD': Money('95.00', 'USD'), # fee ≈ 3.27%
}
PAYIN_CARD_MAX = {k: Money('2500.00', k) for k in ('EUR', 'USD')}
PAYIN_DIRECT_DEBIT_MIN_EUR_GBP = Decimal('25.00') # fee ≈ 3.6%
PAYIN_DIRECT_DEBIT_MIN = {
'EUR': Money(PAYIN_DIRECT_DEBIT_MIN_EUR_GBP, 'EUR'),
'GBP': Money(PAYIN_DIRECT_DEBIT_MIN_EUR_GBP, 'GBP'),
}
PAYIN_DIRECT_DEBIT_TARGET_EUR_GBP = Decimal('99.00') # fee ≈ 0.94%
PAYIN_DIRECT_DEBIT_TARGET = {
'EUR': Money(PAYIN_DIRECT_DEBIT_TARGET_EUR_GBP, 'EUR'),
'GBP': Money(PAYIN_DIRECT_DEBIT_TARGET_EUR_GBP, 'GBP'),
}
PAYIN_DIRECT_DEBIT_MAX = {k: Money('2500.00', k) for k in ('EUR', 'USD')}

PERIOD_CONVERSION_RATES = {
'weekly': Decimal(1),
Expand Down
17 changes: 9 additions & 8 deletions tests/py/test_payio.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,29 +307,29 @@ def test_upcharge_basically_works(self):

def test_upcharge_full_in_rounded_case(self):
actual = upcharge_card(EUR('5.00'))
expected = upcharge_card(EUR(PAYIN_CARD_MIN))
expected = upcharge_card(PAYIN_CARD_MIN['EUR'])
assert actual == expected

def test_upcharge_at_min(self):
actual = upcharge_card(EUR(PAYIN_CARD_MIN))
actual = upcharge_card(PAYIN_CARD_MIN['EUR'])
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(EUR(PAYIN_CARD_TARGET))
actual = upcharge_card(PAYIN_CARD_TARGET['EUR'])
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(EUR('0.01'))
expected = upcharge_card(EUR(PAYIN_CARD_MIN))
expected = upcharge_card(PAYIN_CARD_MIN['EUR'])
assert actual == expected

def test_upcharge_at_min_minus_one_cent(self):
actual = upcharge_card(EUR(PAYIN_CARD_MIN) - EUR('0.01'))
expected = upcharge_card(EUR(PAYIN_CARD_MIN))
actual = upcharge_card(PAYIN_CARD_MIN['EUR'] - EUR('0.01'))
expected = upcharge_card(PAYIN_CARD_MIN['EUR'])
assert actual == expected

def test_skim_credit(self):
Expand Down Expand Up @@ -442,16 +442,17 @@ def throw_transactions_back_in_time(self):
""")

def test_1_sync_with_mangopay_records_exchange_success(self):
amount = PAYIN_CARD_MIN['EUR'].amount
with mock.patch('liberapay.billing.transactions.record_exchange_result') as rer:
rer.side_effect = Foobar()
with self.assertRaises(Foobar):
charge(self.db, self.janet_route, PAYIN_CARD_MIN, 'http://localhost/')
charge(self.db, self.janet_route, amount, 'http://localhost/')
exchange = self.db.one("SELECT * FROM exchanges")
assert exchange.status == 'pre'
sync_with_mangopay(self.db)
exchange = self.db.one("SELECT * FROM exchanges")
assert exchange.status == 'succeeded'
assert Participant.from_username('janet').balance == PAYIN_CARD_MIN
assert Participant.from_username('janet').balance == amount

def test_2_sync_with_mangopay_handles_payins_that_didnt_happen(self):
pass # this is for pep8
Expand Down
18 changes: 10 additions & 8 deletions www/%username/wallet/payin/bankwire/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ from liberapay.exceptions import InvalidNumber
from liberapay.utils import b64decode_s, get_participant
from liberapay.utils.i18n import Money

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

Expand Down Expand Up @@ -53,13 +50,16 @@ if request.method == 'POST' and request.body.get('action') == 'email':
exchange, payin = None, None

funded = float('inf')
currency = request.qs.get('currency', currency)
if currency not in PAYIN_BANK_WIRE_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
donations = participant.get_giving_for_profile()[1]
weekly = donations - participant.receiving
if weekly > 0:
funded = participant.balance // weekly
min_weeks = (constants.PAYIN_BANK_WIRE_MIN / weekly).to_integral_value(ROUND_UP)
min_weeks = (constants.PAYIN_BANK_WIRE_MIN[currency].amount / weekly).to_integral_value(ROUND_UP)
max_weeks = min(
max(constants.PAYIN_BANK_WIRE_TARGET // weekly, 52),
max(constants.PAYIN_BANK_WIRE_TARGET[currency].amount // weekly, 52),
constants.KYC_PAYIN_YEARLY_THRESHOLD // weekly
)
weeks_list = sorted(set((min_weeks, 4, 13, 26, 39, max_weeks)))
Expand All @@ -80,13 +80,15 @@ if request.method == 'POST':
except:
pass
try:
amount = D(amount)
amount = Money(amount, currency)
except (InvalidOperation, ValueError):
raise InvalidNumber(amount)
if amount < AMOUNT_MIN.amount or amount > AMOUNT_MAX.amount:
amount_min = upcharge_bank_wire(constants.PAYIN_BANK_WIRE_MIN[currency])[0]
amount_max = upcharge_bank_wire(constants.PAYIN_BANK_WIRE_MAX[currency])[0]
if amount < amount_min or amount > amount_max:
raise response.error(400, _(
"'{0}' is not an acceptable amount (min={1}, max={2})",
amount, AMOUNT_MIN, AMOUNT_MAX
amount, amount_min, amount_max
))
payin, exchange = payin_bank_wire(website.db, participant, amount)
redir = request.path.raw+'?exchange_id=%s' % exchange.id
Expand Down
21 changes: 12 additions & 9 deletions www/%username/wallet/payin/card/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ from liberapay.models.exchange_route import ExchangeRoute
from liberapay.utils import b64decode_s, get_participant, is_card_expired
from liberapay.utils.i18n import Money

AMOUNT_MIN = Money(PAYIN_CARD_MIN, 'EUR')
AMOUNT_MAX = skim_amount(Money(KYC_PAYIN_YEARLY_THRESHOLD, 'EUR'), FEE_PAYIN_CARD['EUR'])[0]

[---]

participant = get_participant(state, restrict=True, block_suspended_user=True)
Expand Down Expand Up @@ -50,14 +47,19 @@ if card:
card = None

funded = float('inf')
currency = request.qs.get('currency', currency)
if currency not in PAYIN_CARD_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
donations = participant.get_giving_for_profile()[1]
weekly = donations - participant.receiving
amount_min = PAYIN_CARD_MIN[currency]
amount_max = skim_amount(constants.PAYIN_CARD_MAX[currency], FEE_PAYIN_CARD[currency])[0]
if weekly > 0:
funded = participant.balance // weekly
min_weeks = (constants.PAYIN_CARD_MIN / weekly).to_integral_value(ROUND_UP)
min_weeks = (amount_min.amount / weekly).to_integral_value(ROUND_UP)
max_weeks = min(
max(constants.PAYIN_CARD_TARGET // weekly, 52),
AMOUNT_MAX.amount // weekly
max(constants.PAYIN_CARD_TARGET[currency].amount // weekly, 52),
amount_max.amount // weekly
)
weeks_list = sorted(set((min_weeks, 4, 13, 26, 39, max_weeks)))
weeks_list = [w for w in weeks_list if w >= min_weeks and w <= max_weeks]
Expand All @@ -82,10 +84,11 @@ if request.method == 'POST':
amount = D(amount)
except (InvalidOperation, ValueError):
raise InvalidNumber(amount)
if amount < AMOUNT_MIN.amount or amount > AMOUNT_MAX.amount:
amount = Money(amount, currency)
if amount < amount_min.amount or amount > amount_max.amount:
raise response.error(400, _(
"'{0}' is not an acceptable amount (min={1}, max={2})",
amount, AMOUNT_MIN, AMOUNT_MAX
amount, amount_min, amount_max
))
return_url = website.canonical_url + request.path.raw
try:
Expand Down Expand Up @@ -177,7 +180,7 @@ title = _("Adding Money")
% set months = _months.quantize(D('1'))
<li class="list-group-item">
<label>
<input type="radio" name="amount" value="{{ amount }}" class="hidden-xs" />
<input type="radio" name="amount" value="{{ amount.amount }}" class="hidden-xs" />
<div class="radio-label">
<h5 class="list-group-item-heading">{{ _(
"{0} ({2}% fee included)",
Expand Down
20 changes: 11 additions & 9 deletions www/%username/wallet/payin/direct-debit/%exchange_id.spt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ from liberapay.utils.i18n import Money
# https://docs.mangopay.com/endpoints/v2.01/mandates#e231_create-a-mandate
MANDATE_LANGS = {'en', 'fr', 'nl', 'de', 'es', 'it', 'pl'}

AMOUNT_MIN = Money(PAYIN_DIRECT_DEBIT_MIN, 'EUR')
AMOUNT_MAX = skim_amount(Money(KYC_PAYIN_YEARLY_THRESHOLD, 'EUR'), FEE_PAYIN_DIRECT_DEBIT['EUR'])[0]

[---]

participant = get_participant(state, restrict=True, block_suspended_user=True)
Expand Down Expand Up @@ -62,12 +59,17 @@ if route is not None:
funded = float('inf')
donations = participant.get_giving_for_profile()[1]
weekly = donations - participant.receiving
currency = request.qs.get('currency', currency)
if currency not in PAYIN_DIRECT_DEBIT_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
amount_min = PAYIN_DIRECT_DEBIT_MIN[currency]
amount_max = skim_amount(constants.PAYIN_DIRECT_DEBIT_MAX[currency], FEE_PAYIN_DIRECT_DEBIT[currency])[0]
if weekly > 0:
funded = participant.balance // weekly
min_weeks = (PAYIN_DIRECT_DEBIT_MIN / weekly).to_integral_value(ROUND_UP)
min_weeks = (amount_min.amount / weekly).to_integral_value(ROUND_UP)
max_weeks = min(
max(constants.PAYIN_DIRECT_DEBIT_TARGET // weekly, 52),
AMOUNT_MAX.amount // weekly
max(constants.PAYIN_DIRECT_DEBIT_TARGET[currency] // weekly, 52),
amount_max.amount // weekly
)
weeks_list = sorted(set((min_weeks, 4, 13, 26, 39, max_weeks)))
weeks_list = [w for w in weeks_list if w >= min_weeks and w <= max_weeks]
Expand All @@ -89,13 +91,13 @@ if request.method == 'POST':
except:
pass
try:
amount = D(amount)
amount = Money(amount, currency)
except (InvalidOperation, ValueError):
raise InvalidNumber(amount)
if amount < AMOUNT_MIN.amount or amount > AMOUNT_MAX.amount:
if amount < amount_min or amount > amount_max:
raise response.error(400, _(
"'{0}' is not an acceptable amount (min={1}, max={2})",
amount, AMOUNT_MIN, AMOUNT_MAX
amount, amount_min, amount_max
))
mandate = Mandate.get(route.mandate) if route.mandate else None
if mandate and mandate.Status == 'FAILED':
Expand Down
2 changes: 1 addition & 1 deletion www/about/faq.spt
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ title = _("Frequently Asked Questions")
"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)."
, constants.DONATION_LIMITS[currency]['weekly'][0]
, Money(constants.PAYIN_CARD_MIN, 'EUR')
, constants.PAYIN_CARD_MIN[currency]
) }}<br>
{{ _(
"The maximum you can give any one user is {0} per week. This helps to "
Expand Down

0 comments on commit f506134

Please sign in to comment.