Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Currencies - part 5 #796

Merged
merged 8 commits into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion liberapay/billing/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,9 @@ def propagate_exchange(cursor, participant, exchange, error, amount):
AND b.ts < now() - INTERVAL %s
AND b.disputed IS NOT TRUE
AND b.locked_for IS NULL
AND b.amount::currency = %s
ORDER BY b.owner = e.participant DESC, b.ts
""", (participant.id, QUARANTINE))
""", (participant.id, QUARANTINE, amount.currency))
withdrawable = sum(b.amount for b in bundles)
x = -amount
if x > withdrawable:
Expand Down
1 change: 1 addition & 0 deletions liberapay/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def _check_bundles_grouped_by_withdrawal_against_exchanges(cursor):
LEFT JOIN (
SELECT b.withdrawal, sum(b.amount) as withdrawn
FROM cash_bundles b
WHERE b.withdrawal IS NOT NULL
GROUP BY b.withdrawal
) AS b ON b.withdrawal = e.id
)
Expand Down
15 changes: 12 additions & 3 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
NS, deserialize, erase_cookie, serialize, set_cookie,
emails, i18n, markdown,
)
from liberapay.utils.currencies import MoneyBasket
from liberapay.website import website


Expand Down Expand Up @@ -1065,8 +1066,8 @@ def get_current_wallet(self, currency=None, create=False):
from liberapay.billing.transactions import create_wallet
return create_wallet(self.db, self, currency)

def get_current_wallets(self):
return self.db.all("""
def get_current_wallets(self, cursor=None):
return (cursor or self.db).all("""
SELECT *
FROM wallets
WHERE owner = %s
Expand All @@ -1082,6 +1083,14 @@ def get_balance_in(self, currency):
AND is_current
""", (self.id, currency)) or ZERO[currency]

def get_balances(self):
return self.db.one("""
SELECT basket_sum(balance)
FROM wallets
WHERE owner = %s
AND is_current
""", (self.id,)) or MoneyBasket()


# Events
# ======
Expand Down Expand Up @@ -1552,7 +1561,7 @@ def update_giving(self, cursor=None):
ORDER BY p2.join_time IS NULL, t.ctime ASC
""", (self.id,))
updated = []
for wallet in self.get_current_wallets():
for wallet in self.get_current_wallets(cursor):
fake_balance = wallet.balance
currency = fake_balance.currency
fake_balance += self.get_receiving_in(currency, cursor)
Expand Down
4 changes: 4 additions & 0 deletions liberapay/utils/currencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ def __bool__(self):

__nonzero__ = __bool__

@property
def currencies_present(self):
return [m.currency for m in self if m.amount]

@classmethod
def sum(cls, amounts):
r = cls(Money('0.00', 'EUR'), usd=Money('0.00', 'USD'))
Expand Down
2 changes: 1 addition & 1 deletion liberapay/utils/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def format_decimal(self, *a):
return format_decimal(*a, locale=self)

def format_money_basket(self, basket):
return ', '.join(
return ' + '.join(
format_currency(money.amount, money.currency, locale=self)
for money in basket if money
) or '0'
Expand Down
11 changes: 4 additions & 7 deletions www/%username/settings/close.spt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pending_payouts = website.db.one("""
AND status = 'created'
""", (participant.id,))

balances = participant.get_balances()

title = _("Close Account")
subhead = participant.username

Expand Down Expand Up @@ -60,22 +62,17 @@ subhead = participant.username
<h3>{{ _("Wallet") }}</h3>

<p>{{ _("You have {0} in your wallet. What should we do with it?",
participant.balance) }}</p>

balances) }}</p>
<ul>

<li><a href="{{ participant.path('wallet/payout/'+b64encode_s(request.line.uri)) }}"
>{{ _("Withdraw it") }}</a></li>

<li><label>
<input type="radio" name="disburse_to" value="downstream"
{{ 'disabled' if not participant.get_giving_for_profile()[1] else '' }} />
{{ 'disabled' if not participant.giving else '' }} />
{{ _("Give it to the {0}people I donate to{1}",
'<a href="%s">'|safe % participant.path('giving'), '</a>'|safe) }}
</label></li>

</ul>

<p>{{ _(
"If neither option works for you, please contact [email protected]."
) }}</p>
Expand Down
5 changes: 4 additions & 1 deletion www/%username/tip.spt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ if request.method == 'POST':
back_to += '&' if '?' in back_to else '?'
back_to += 'success=' + b64encode_s(out["msg"])
if out['amount'] and not out['is_funded'] and not out['is_pledge']:
response.redirect('/' + tipper.username + '/wallet/payin/' + b64encode_s(back_to))
response.redirect(
'/' + tipper.username + '/wallet/payin/' + b64encode_s(back_to) +
'?currency=' + currency
)
response.redirect(back_to, trusted_url=False)
else:
out = tipper.get_tip_to(tippee)
Expand Down
8 changes: 6 additions & 2 deletions www/%username/wallet/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,15 @@ else:
locale.format_money_basket(event['balances'])
}}</td>
<td class="wallet">
% for delta in event['wallet_deltas']
% if delta.amount or delta.currency == participant.main_currency
% set wallet_deltas = event['wallet_deltas']
% if wallet_deltas
% set n = len(wallet_deltas.currencies_present)
% for delta in wallet_deltas
% if delta.amount or n == 0 and delta.currency == participant.main_currency
{{ format_money_delta(delta) }}<br>
% endif
% endfor
% endif
</td>
<td class="fees"></td>
<td class="bank"></td>
Expand Down
6 changes: 3 additions & 3 deletions www/%username/wallet/payin/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ title = _("Adding Money")
{{ _("Easy and instantaneous") }}<br>
{{ _("Fees: {0}% + {1}", *constants.FEE_PAYIN_CARD[currency].with_vat) }}
</p>
<a class="overlay" href="{{ base_path }}/card/{{ b64encode_s(back_to) }}"></a>
<a class="overlay" href="{{ base_path }}/card/{{ b64encode_s(back_to) }}?currency={{ currency }}"></a>
</div></div>
</div>

Expand All @@ -93,7 +93,7 @@ title = _("Adding Money")
_("Cheaper¹ but cumbersome") }}<br>
{{ _("Fee: {0}%", constants.FEE_PAYIN_BANK_WIRE.with_vat) }}
</p>
<a class="overlay" href="{{ base_path }}/bankwire/{{ b64encode_s(back_to) }}"></a>
<a class="overlay" href="{{ base_path }}/bankwire/{{ b64encode_s(back_to) }}?currency={{ currency }}"></a>
</div></div>
</div>

Expand All @@ -106,7 +106,7 @@ title = _("Adding Money")
{{ _("Best for regular payments") }}<br>
{{ _("Fee: {0}", constants.FEE_PAYIN_DIRECT_DEBIT[currency].with_vat) }}
</p>
<a class="overlay" href="{{ base_path }}/direct-debit/"></a>
<a class="overlay" href="{{ base_path }}/direct-debit/?currency={{ currency }}"></a>
</div></div>
</div>
% endif
Expand Down
8 changes: 6 additions & 2 deletions www/%username/wallet/payin/bankwire/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ if request.method == 'POST' and request.body.get('action') == 'email':

exchange, payin = None, None

funded = float('inf')
currency = request.qs.get('currency', currency)
if request.method == 'POST':
currency = request.body.get('currency', currency)
if currency not in PAYIN_BANK_WIRE_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
raise response.error(400, "`currency` value '%s' is invalid or non-supported" % currency)

funded = float('inf')
balance = participant.get_balance_in(currency)
donations = participant.get_giving_in(currency)
weekly = donations - participant.get_receiving_in(currency)
Expand Down Expand Up @@ -212,6 +215,7 @@ title = _("Adding Money")

<fieldset id="amount" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<input type="hidden" name="currency" value="{{ currency }}" />
<p>{{ _("Please select a precomputed amount or input a custom one.") }}</p>
<ul class="list-group radio-group">
% for weeks in weeks_list
Expand Down
5 changes: 4 additions & 1 deletion www/%username/wallet/payin/card/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ if request.method == 'GET' and 'transactionId' in request.qs:
response.redirect(request.path.raw+'?exchange_id=%s' % payin.Tag)

currency = request.qs.get('currency', currency)
if request.method == 'POST':
currency = request.body.get('currency', currency)
if currency not in PAYIN_CARD_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
raise response.error(400, "`currency` value '%s' is invalid or non-supported" % currency)

exchange = None
routes = ExchangeRoute.from_network(participant, 'mango-cc', currency=currency)
Expand Down Expand Up @@ -173,6 +175,7 @@ title = _("Adding Money")
<fieldset id="amount" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<input type="hidden" name="route_id" value="{{ route.id if route else '' }}" />
<input type="hidden" name="currency" value="{{ currency }}" />
<p>{{ _("Please select a precomputed amount or input a custom one.") }}</p>
<ul class="list-group radio-group">
% for weeks in weeks_list
Expand Down
8 changes: 6 additions & 2 deletions www/%username/wallet/payin/direct-debit/%exchange_id.spt
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ else:
if route is not None:
route.__dict__['participant'] = participant

funded = float('inf')
currency = request.qs.get('currency', currency)
if request.method == 'POST':
currency = request.body.get('currency', currency)
if currency not in PAYIN_DIRECT_DEBIT_MIN:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")
raise response.error(400, "`currency` value '%s' is invalid or non-supported" % currency)

funded = float('inf')
balance = participant.get_balance_in(currency)
donations = participant.get_giving_in(currency)
weekly = donations - participant.get_receiving_in(currency)
Expand Down Expand Up @@ -222,6 +225,7 @@ title = _("Adding Money")
<fieldset id="amount" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<input type="hidden" name="route_id" value="{{ route.id if route else '' }}" />
<input type="hidden" name="currency" value="{{ currency }}" />
<p>{{ _("Please select a precomputed amount or input a custom one.") }}</p>
<ul class="list-group radio-group">
% for weeks in weeks_list
Expand Down
66 changes: 53 additions & 13 deletions www/%username/wallet/payout/%back_to.spt
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,21 @@ exchange = None
bank_account = None

back_to = b64decode_s(request.path['back_to'], default=None)
currency = request.qs.get('currency', currency)
if currency not in constants.CURRENCIES:
raise response.error(400, "`currency` value in querystring is invalid or non-supported")

currency = request.qs.get('currency')
if request.method == 'POST':
body = request.body
currency = request.body.get('currency', currency or state['currency'])
if currency is not None and currency not in constants.CURRENCIES:
raise response.error(400, "`currency` value '%s' is invalid or non-supported" % currency)

if request.method == 'POST':
amount = Money(parse_decimal(request.body['amount']), currency)
if amount <= 0:
raise response.error(400, _("The amount cannot be zero."))
routes = ExchangeRoute.from_network(participant, 'mango-ba')
if not routes:
raise response.error(400, "no route")
ignore_high_fee = body.get('confirmed') == 'true'
ignore_high_fee = request.body.get('confirmed') == 'true'
try:
exchange = payout(website.db, routes[0], amount, ignore_high_fee)
except TransactionFeeTooHigh as e:
Expand All @@ -54,13 +55,25 @@ if request.method == 'POST':
elif 'exchange_id' in request.qs:
exchange = website.db.one("SELECT * FROM exchanges WHERE id = %s AND participant = %s",
(request.qs['exchange_id'], participant.id))
currency = exchange.amount.currency

if not currency:
balances = participant.get_balances()
currencies = balances.currencies_present
if len(currencies) == 1:
currency = currencies[0]
elif len(currencies) == 0:
currency = state['currency']
else:
show_form = False

balance = participant.get_balance_in(currency)
success = getattr(exchange, 'status', None) in ('created', 'succeeded')
show_form = balance > 0 and not success
if show_form or not success:
withdrawable = participant.get_withdrawable_amount(currency)
show_form = withdrawable > 0
if currency:
balance = participant.get_balance_in(currency)
success = getattr(exchange, 'status', None) in ('created', 'succeeded')
show_form = balance > 0 and not success
if show_form or not success:
withdrawable = participant.get_withdrawable_amount(currency)
show_form = withdrawable > 0

if show_form:
mp_account = participant.get_mangopay_account()
Expand Down Expand Up @@ -100,7 +113,9 @@ title = _("Withdrawing Money")
<form id="payout" action="javascript:" method="POST"
data-msg-loading="{{ _('Request in progress, please wait…') }}">

% if show_form
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
% endif

% if exchange
<div class="alert alert-{{ 'success' if success else 'danger' }}">{{
Expand All @@ -110,6 +125,8 @@ title = _("Withdrawing Money")
_("The attempt to send {0} to your bank account has failed. Error message: {1}", -exchange.amount, exchange.note)
}}</div>
% endif

% if currency
<p>
{{ _("You have {0} in your Liberapay wallet.", balance) }}
% if not success
Expand All @@ -129,15 +146,37 @@ title = _("Withdrawing Money")
<p><a href="{{ response.sanitize_untrusted_url(back_to) }}"
class="btn btn-success">{{ _("Go back") }}</a></p>
% endif

% if show_form
% else
<p>{{ _("You have {0} in your Liberapay wallet.", balances) }}</p>
<p>{{ _("Which currency do you want to withdraw?") }}</p>
<p class="buttons">
% for currency in currencies
<a class="btn btn-default btn-lg" href="?currency={{ currency }}"
>{{ locale.currencies.get(currency, currency) }} ({{ locale.currency_symbols.get(currency, currency) }})</a>
% endfor
</p>
<p>{{ _(
"Withdrawing euros to a SEPA bank account is free, transfers to other "
"countries cost {0} each. Withdrawing US dollars costs {1} per transfer "
"regardless of the destination country.",
constants.FEE_PAYOUT['EUR']['foreign'].with_vat,
constants.FEE_PAYOUT['USD']['*'].with_vat,
) }}</p>
% endif

% if show_form
% if currency == 'EUR'
<p>{{ _(
"Withdrawing euros to a SEPA bank account is free, transfers to other "
"countries cost {0} each.",
constants.FEE_PAYOUT['EUR']['foreign'].with_vat,
) }}</p>
% elif currency == 'USD'
<p>{{ _(
"Withdrawing US dollars costs {0} per transfer, whatever the destination country is.",
constants.FEE_PAYOUT['USD']['*'].with_vat,
) }}</p>
% endif

<h3>{{ _("Amount") }}</h3>

Expand All @@ -148,6 +187,7 @@ title = _("Withdrawing Money")
<fieldset id="amount" class="form-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<input type="hidden" name="route_id" value="{{ routes[0].id if routes else '' }}" />
<input type="hidden" name="currency" value="{{ currency }}" />
<div class="input-group">
<input name="amount" value="{{ format_decimal(recommended_withdrawal) }}"
class="form-control" size=6 required />
Expand Down