Skip to content

Commit

Permalink
Merge pull request #1286 from liberapay/currencies-v2
Browse files Browse the repository at this point in the history
This branch closes #833 by adding support for 31 more currencies. It also closes #873 and partially addresses #1262.
  • Loading branch information
Changaco authored Oct 23, 2018
2 parents 7a3ad0f + 62e0a5f commit 7b9db6d
Show file tree
Hide file tree
Showing 35 changed files with 747 additions and 231 deletions.
10 changes: 7 additions & 3 deletions liberapay/billing/payday.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ def prepare(cursor, ts_start):
, username
, join_time
, ( COALESCE((eur_w.balance).amount, '0.00'),
COALESCE((usd_w.balance).amount, '0.00')
COALESCE((usd_w.balance).amount, '0.00'),
NULL
)::currency_basket AS balances
, goal
, kind
Expand Down Expand Up @@ -423,7 +424,10 @@ def resolve_takes(tips, takes, ref_currency):
take.amount = (take.amount * takes_ratio).round_up()
if take.paid_in_advance is None:
take.paid_in_advance = take.amount.zero()
take.accepted_currencies = take.accepted_currencies.split(',')
if take.accepted_currencies is None:
take.accepted_currencies = constants.CURRENCIES
else:
take.accepted_currencies = take.accepted_currencies.split(',')
for accepted in take.accepted_currencies:
skip = (
accepted == take.main_currency or
Expand Down Expand Up @@ -586,7 +590,7 @@ def check_balances(cursor):
, p2.balances
FROM payday_participants p2
JOIN participants p ON p.id = p2.id
WHERE (p2.balances).EUR < 0 OR (p2.balances).USD < 0
WHERE p2.balances->'EUR' < 0 OR p2.balances->'USD' < 0
LIMIT 1
""")
if oops:
Expand Down
36 changes: 21 additions & 15 deletions liberapay/billing/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
QUARANTINE = '%s days' % QUARANTINE.days


def Money_to_cents(m):
r = Money(currency=m.currency)
r.amount = int(m.amount * 100)
return r


def repr_error(o):
r = o.ResultCode
if r == '000000':
Expand Down Expand Up @@ -98,9 +104,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 = amount.int()
payout.DebitedFunds = Money_to_cents(amount)
payout.DebitedWalletId = participant.get_current_wallet(amount.currency).remote_id
payout.Fees = fee.int()
payout.Fees = Money_to_cents(fee)
payout.BankAccountId = route.address
payout.BankWireRef = str(e_id)
payout.Tag = str(e_id)
Expand Down Expand Up @@ -139,11 +145,11 @@ def charge(db, route, amount, return_url, billing_address=None):
if billing_address:
payin.Billing = {'Address': billing_address}
payin.CreditedWalletId = wallet.remote_id
payin.DebitedFunds = charge_amount.int()
payin.DebitedFunds = Money_to_cents(charge_amount)
payin.CardId = route.address
payin.SecureMode = 'FORCE'
payin.SecureModeReturnURL = return_url
payin.Fees = fee.int()
payin.Fees = Money_to_cents(fee)
payin.Tag = str(e_id)
try:
test_hook()
Expand Down Expand Up @@ -207,9 +213,9 @@ def execute_direct_debit(db, exchange, route):
payin = DirectDebitDirectPayIn()
payin.AuthorId = participant.mangopay_user_id
payin.CreditedWalletId = exchange.wallet_id
payin.DebitedFunds = debit_amount.int()
payin.DebitedFunds = Money_to_cents(debit_amount)
payin.MandateId = route.mandate
payin.Fees = fee.int()
payin.Fees = Money_to_cents(fee)
payin.Tag = str(e_id)
try:
test_hook()
Expand Down Expand Up @@ -241,8 +247,8 @@ def payin_bank_wire(db, participant, debit_amount):
payin = BankWirePayIn()
payin.AuthorId = participant.mangopay_user_id
payin.CreditedWalletId = wallet.remote_id
payin.DeclaredDebitedFunds = debit_amount.int()
payin.DeclaredFees = fee.int()
payin.DeclaredDebitedFunds = Money_to_cents(debit_amount)
payin.DeclaredFees = Money_to_cents(fee)
payin.Tag = str(e_id)
try:
test_hook()
Expand Down Expand Up @@ -505,7 +511,7 @@ def transfer(db, tipper, tippee, amount, context, **kw):
tr.AuthorId = tipper_wallet.remote_owner_id
tr.CreditedUserId = tippee_wallet.remote_owner_id
tr.CreditedWalletId = wallet_to
tr.DebitedFunds = amount.int()
tr.DebitedFunds = Money_to_cents(amount)
tr.DebitedWalletId = wallet_from
tr.Fees = Money(0, amount.currency)
tr.Tag = str(t_id)
Expand Down Expand Up @@ -611,7 +617,7 @@ def initiate_transfer(db, t_id):
tr.AuthorId = tipper_wallet.remote_owner_id
tr.CreditedUserId = tippee_wallet.remote_owner_id
tr.CreditedWalletId = tippee_wallet.remote_id
tr.DebitedFunds = amount.int()
tr.DebitedFunds = Money_to_cents(amount)
tr.DebitedWalletId = tipper_wallet.remote_id
tr.Fees = Money(0, amount.currency)
tr.Tag = str(t_id)
Expand Down Expand Up @@ -774,8 +780,8 @@ def refund_payin(db, exchange, amount, participant):
m_refund = PayInRefund(payin_id=exchange.remote_id)
m_refund.AuthorId = wallet.remote_owner_id
m_refund.Tag = str(e_refund.id)
m_refund.DebitedFunds = amount.int()
m_refund.Fees = -fee.int()
m_refund.DebitedFunds = Money_to_cents(amount)
m_refund.Fees = -Money_to_cents(fee)
try:
m_refund.save()
except Exception as e:
Expand Down Expand Up @@ -926,8 +932,8 @@ def refund_disputed_payin(db, exchange, create_debts=False, refund_fee=False, dr
m_refund = PayInRefund(payin_id=exchange.remote_id)
m_refund.AuthorId = wallet.remote_owner_id
m_refund.Tag = str(e_refund.id)
m_refund.DebitedFunds = amount.int()
m_refund.Fees = -fee.int()
m_refund.DebitedFunds = Money_to_cents(amount)
m_refund.Fees = -Money_to_cents(fee)
try:
m_refund.save()
except Exception as e:
Expand Down Expand Up @@ -1020,7 +1026,7 @@ def recover_lost_funds(db, exchange, lost_amount, repudiation_id):
tr.AuthorId = original_owner.mangopay_user_id
tr.CreditedUserId = chargebacks_account.mangopay_user_id
tr.CreditedWalletId = to_wallet
tr.DebitedFunds = exchange.amount.int()
tr.DebitedFunds = Money_to_cents(exchange.amount)
tr.DebitedWalletId = from_wallet
tr.Fees = Money(0, currency)
tr.RepudiationId = repudiation_id
Expand Down
105 changes: 78 additions & 27 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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')


Expand All @@ -64,14 +92,28 @@ 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_UNIT = Decimal('1.00')
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,
Expand All @@ -80,10 +122,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'''
^
Expand Down Expand Up @@ -233,39 +275,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"),
Expand Down Expand Up @@ -367,17 +409,28 @@ 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')),
(_("Medium"), Decimal('1.00')),
(_("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

Expand All @@ -386,6 +439,4 @@ def make_standard_tip(label, weekly, currency):
USERNAME_MAX_SIZE = 32
USERNAME_SUFFIX_BLACKLIST = set('.txt .html .htm .json .xml'.split())

ZERO = {c: Money(D_ZERO, c) for c in ('EUR', 'USD', None)}

del _
3 changes: 1 addition & 2 deletions liberapay/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import aspen
import aspen.http.mapping
from aspen.request_processor.dispatcher import DispatchResult, DispatchStatus
from mangopay.utils import Money
import pando
from pando import json
from pando.algorithms.website import fill_response_with_output
Expand All @@ -31,7 +30,7 @@
from liberapay.utils import (
b64decode_s, b64encode_s, erase_cookie, http_caching, i18n, set_cookie, urlquote,
)
from liberapay.utils.currencies import MoneyBasket, fetch_currency_exchange_rates
from liberapay.utils.currencies import Money, MoneyBasket, fetch_currency_exchange_rates
from liberapay.utils.emails import handle_email_bounces
from liberapay.utils.state_chain import (
attach_environ_to_request, create_response_object, reject_requests_bypassing_proxy,
Expand Down
8 changes: 4 additions & 4 deletions liberapay/models/_mixin_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections import OrderedDict
from statistics import median

from liberapay.constants import ZERO, TAKE_THROTTLING_THRESHOLD
from liberapay.constants import TAKE_THROTTLING_THRESHOLD
from liberapay.utils import NS, group_by
from liberapay.utils.currencies import Money, MoneyBasket

Expand Down Expand Up @@ -39,7 +39,7 @@ def add_member(self, member, cursor=None):
raise MemberLimitReached
if member.status != 'active':
raise InactiveParticipantAdded
self.set_take_for(member, ZERO[member.main_currency], self, cursor=cursor)
self.set_take_for(member, Money.ZEROS[member.main_currency], self, cursor=cursor)

def remove_all_members(self, cursor=None):
(cursor or self.db).run("""
Expand Down Expand Up @@ -95,7 +95,7 @@ def compute_max_this_week(self, member_id, last_week, currency):
leftover, or last week's median take, or one currency unit (e.g. €1.00).
"""
nonzero_last_week = [a.convert(currency).amount for a in last_week.values() if a]
member_last_week = last_week.get(member_id, ZERO[currency]).convert(currency)
member_last_week = last_week.get(member_id, Money.ZEROS[currency]).convert(currency)
return max(
member_last_week * 2,
member_last_week + last_week.initial_leftover.fuzzy_sum(currency),
Expand Down Expand Up @@ -289,7 +289,7 @@ def get_members(self):
compute_max = self.throttle_takes and nmembers > 1 and last_week.sum
members = OrderedDict()
members.leftover = self.leftover
zero = ZERO[self.main_currency]
zero = Money.ZEROS[self.main_currency]
for take in takes:
member = {}
m_id = member['id'] = take['member_id']
Expand Down
Loading

0 comments on commit 7b9db6d

Please sign in to comment.