Skip to content

Commit

Permalink
improve handling of currency exponents
Browse files Browse the repository at this point in the history
  • Loading branch information
Changaco committed Oct 22, 2018
1 parent 7a3ad0f commit 3781fc0
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 72 deletions.
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
3 changes: 0 additions & 3 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def with_vat(self):
D_CENT = Decimal('0.01')
D_INF = Decimal('inf')
D_MAX = Decimal('999999999999.99')
D_UNIT = Decimal('1.00')
D_ZERO = Decimal('0.00')

DONATION_LIMITS_WEEKLY_EUR_USD = (Decimal('0.01'), Decimal('100.00'))
Expand Down Expand Up @@ -386,6 +385,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 _
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
32 changes: 16 additions & 16 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from base64 import b64decode, b64encode
from datetime import timedelta
from decimal import Decimal
from email.utils import formataddr
from hashlib import pbkdf2_hmac, md5, sha1
from os import urandom
Expand All @@ -16,7 +17,6 @@
from cached_property import cached_property
from html2text import html2text
import mangopay
from mangopay.utils import Money
from markupsafe import escape as htmlescape
from pando import json
from pando.utils import utcnow
Expand All @@ -25,12 +25,12 @@
import requests

from liberapay.constants import (
ASCII_ALLOWED_IN_USERNAME, AVATAR_QUERY, CURRENCIES, D_UNIT, D_ZERO,
ASCII_ALLOWED_IN_USERNAME, AVATAR_QUERY, CURRENCIES,
DONATION_LIMITS, EMAIL_VERIFICATION_TIMEOUT, EVENTS, HTML_A,
PASSWORD_MAX_SIZE, PASSWORD_MIN_SIZE, PAYMENT_SLUGS,
PERIOD_CONVERSION_RATES, PRIVILEGES, PROFILE_VISIBILITY_ATTRS,
PUBLIC_NAME_MAX_SIZE, SESSION, SESSION_REFRESH, SESSION_TIMEOUT,
USERNAME_MAX_SIZE, USERNAME_SUFFIX_BLACKLIST, ZERO,
USERNAME_MAX_SIZE, USERNAME_SUFFIX_BLACKLIST,
)
from liberapay.exceptions import (
BadAmount,
Expand Down Expand Up @@ -73,7 +73,7 @@
NS, deserialize, erase_cookie, serialize, set_cookie, urlquote,
emails, i18n, markdown,
)
from liberapay.utils.currencies import MoneyBasket
from liberapay.utils.currencies import Money, MoneyBasket
from liberapay.utils.emails import check_email_blacklist, normalize_email_address
from liberapay.website import website

Expand Down Expand Up @@ -295,7 +295,7 @@ def get_chargebacks_account(cls, currency):
ON CONFLICT (remote_id) DO UPDATE
SET remote_owner_id = 'CREDIT' -- dummy update
RETURNING *
""", ('CREDIT_' + currency, ZERO[currency], p.id))
""", ('CREDIT_' + currency, Money.ZEROS[currency], p.id))
return p, wallet

def refetch(self):
Expand Down Expand Up @@ -584,7 +584,7 @@ def distribute_balances_to_donees(self, final_gift=True):
]
if len(takes) == 1 and len(tip.takes) == 1 and tip.takes[0]['amount'] == 0:
# Team of one with a zero take
tip.takes[0]['amount'].amount = D_UNIT
tip.takes[0]['amount'].amount = Decimal('1')
tip.total_takes = MoneyBasket(*[t['amount'] for t in tip.takes])
tips = [t for t in tips if getattr(t, 'total_takes', -1) != 0]
transfers = []
Expand All @@ -598,7 +598,7 @@ def distribute_balances_to_donees(self, final_gift=True):
key=lambda t: (t.amount, t.ctime), reverse=True
)
total = Money.sum((t.amount for t in tips_in_this_currency), currency)
distributed = ZERO[currency]
distributed = Money.ZEROS[currency]
initial_balance = wallet.balance
transfers_in_this_currency = []

Expand Down Expand Up @@ -1268,7 +1268,7 @@ def get_withdrawable_amount(self, currency):
AND disputed IS NOT TRUE
AND locked_for IS NULL
AND (amount).currency = %s
""", (self.id, QUARANTINE, currency)) or ZERO[currency]
""", (self.id, QUARANTINE, currency)) or Money.ZEROS[currency]

def can_withdraw(self, amount):
return self.get_withdrawable_amount(amount.currency) >= amount
Expand Down Expand Up @@ -1302,7 +1302,7 @@ def get_balance_in(self, currency):
WHERE owner = %s
AND balance::currency = %s
AND is_current
""", (self.id, currency)) or ZERO[currency]
""", (self.id, currency)) or Money.ZEROS[currency]

def get_balances(self):
return self.db.one("""
Expand Down Expand Up @@ -1840,7 +1840,7 @@ def get_giving_in(self, currency):
AND t.amount::currency = %s
AND p.status = 'active'
AND (p.goal IS NULL OR p.goal >= 0)
""", (self.id, currency)) or ZERO[currency]
""", (self.id, currency)) or Money.ZEROS[currency]

def get_receiving_in(self, currency, cursor=None):
r = (cursor or self.db).one("""
Expand All @@ -1849,14 +1849,14 @@ def get_receiving_in(self, currency, cursor=None):
WHERE t.tippee = %s
AND t.amount::currency = %s
AND t.is_funded
""", (self.id, currency)) or ZERO[currency]
""", (self.id, currency)) or Money.ZEROS[currency]
if currency not in CURRENCIES:
raise ValueError(currency)
r += Money((cursor or self.db).one("""
SELECT sum((t.actual_amount).{0})
FROM current_takes t
WHERE t.member = %s
""".format(currency), (self.id,)) or D_ZERO, currency)
""".format(currency), (self.id,)) or Money.ZEROS[currency].amount, currency)
return r

def get_exact_receiving(self):
Expand Down Expand Up @@ -1890,7 +1890,7 @@ def update_giving(self, cursor=None):
currencies = set(t.amount.currency for t in tips)
balances = {w.balance.currency: w.balance for w in self.get_current_wallets(cursor)}
for currency in currencies:
fake_balance = balances.get(currency, ZERO[currency])
fake_balance = balances.get(currency, Money.ZEROS[currency])
fake_balance += self.get_receiving_in(currency, cursor)
for tip in (t for t in tips if t.amount.currency == currency):
if tip.amount <= (tip.paid_in_advance or 0):
Expand Down Expand Up @@ -1939,7 +1939,7 @@ def update_receiving(self, cursor=None):
with self.db.get_cursor(cursor) as c:
if self.kind == 'group':
c.run("LOCK TABLE takes IN EXCLUSIVE MODE")
zero = ZERO[self.main_currency]
zero = Money.ZEROS[self.main_currency]
r = c.one("""
WITH our_tips AS (
SELECT amount
Expand Down Expand Up @@ -2042,7 +2042,7 @@ def _zero_tip_dict(tippee, currency=None):
tippee = Participant.from_id(tippee)
if not currency or currency not in tippee.accepted_currencies:
currency = tippee.main_currency
zero = ZERO[currency]
zero = Money.ZEROS[currency]
return dict(amount=zero, is_funded=False, tippee=tippee.id,
period='weekly', periodic_amount=zero)

Expand Down Expand Up @@ -2107,7 +2107,7 @@ def get_tip_distribution(self):
tip_amounts = []
npatrons = 0
currency = self.main_currency
contributed = ZERO[currency]
contributed = Money.ZEROS[currency]
for rec in recs:
tip_amounts.append([
rec.amount,
Expand Down
27 changes: 24 additions & 3 deletions liberapay/payin/stripe.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals

from decimal import Decimal

import stripe
import stripe.error

Expand All @@ -8,6 +10,25 @@
from .common import update_payin, update_payin_transfer


# https://stripe.com/docs/currencies#presentment-currencies
ZERO_DECIMAL_CURRENCIES = """
BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF
""".split()


def int_to_Money(amount, currency):
currency = currency.upper()
if currency in ZERO_DECIMAL_CURRENCIES:
return Money(Decimal(amount), currency)
return Money(Decimal(amount) / 100, currency)


def Money_to_int(m):
if m.currency in ZERO_DECIMAL_CURRENCIES:
return int(m.amount)
return int(m.amount * 100)


def repr_stripe_error(e):
"""Given a `StripeError` exception, return an error message suitable for display.
"""
Expand Down Expand Up @@ -45,7 +66,7 @@ def destination_charge(db, payin, payer, statement_descriptor):
destination = {'account': destination}
try:
charge = stripe.Charge.create(
amount=amount.int().amount,
amount=Money_to_int(amount),
currency=amount.currency.lower(),
customer=route.remote_user_id,
destination=destination,
Expand All @@ -63,8 +84,8 @@ def destination_charge(db, payin, payer, statement_descriptor):
return update_payin(db, payin.id, '', 'failed', str(e))

bt = charge.balance_transaction
amount_settled = Money(bt.amount, bt.currency.upper()) / 100
fee = Money(bt.fee, bt.currency.upper()) / 100
amount_settled = int_to_Money(bt.amount, bt.currency)
fee = int_to_Money(bt.fee, bt.currency)
net_amount = amount_settled - fee

if destination:
Expand Down
Loading

0 comments on commit 3781fc0

Please sign in to comment.