diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-01-16 18:11:35 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-01-16 18:11:40 +0100 |
commit | 47ff439abf85ecea69d7f281ca30a00143c8a868 (patch) | |
tree | f6bde35d3dd4fbe962c726b3dc2e032adfe7f223 /talerbank/app/views.py | |
parent | 55addfc68f26f50425e3af84972f4d342a99939f (diff) | |
download | bank-47ff439abf85ecea69d7f281ca30a00143c8a868.tar.gz bank-47ff439abf85ecea69d7f281ca30a00143c8a868.tar.bz2 bank-47ff439abf85ecea69d7f281ca30a00143c8a868.zip |
preparations towards the new bank API
Diffstat (limited to 'talerbank/app/views.py')
-rw-r--r-- | talerbank/app/views.py | 273 |
1 files changed, 87 insertions, 186 deletions
diff --git a/talerbank/app/views.py b/talerbank/app/views.py index a6902be..3df8505 100644 --- a/talerbank/app/views.py +++ b/talerbank/app/views.py @@ -45,7 +45,7 @@ from django.shortcuts import render, redirect from django.core.exceptions import ObjectDoesNotExist from datetime import datetime from .models import BankAccount, BankTransaction, TalerWithdrawOperation -from taler.util.amount import Amount +from taler.util.amount import Amount, SignedAmount import qrcode import qrcode.image.svg import lxml @@ -53,7 +53,6 @@ from .schemas import ( HistoryParams, HistoryRangeParams, URLParamValidationError, - RejectData, AddIncomingData, JSONFieldException, InvalidSession, @@ -129,18 +128,6 @@ class SameAccountException(Exception): self.taler_error_code = 5102 -## -# Exception raised when someone tries to reject a -# transaction, but they have no rights to accomplish -# such operation. -class RejectNoRightsException(Exception): - def __init__(self, msg): - super(RejectNoRightsException, self).__init__(msg) - self.hint = "Only original payer can reject." - self.http_status_code = 403 - self.taler_error_code = 5200 - - class UnhandledException(Exception): def __init__(self, msg="Unhandled exception happened!"): super(UnhandledException, self).__init__(msg) @@ -278,11 +265,12 @@ class InputDatalist(forms.TextInput): return html + datalist -## -# Form for sending wire transfers. It usually appears in the -# user profile page. -# class WTForm(forms.Form): + """ + Form for sending wire transfers. It usually appears in the + user profile page. + """ + amount = forms.FloatField( min_value=0.1, widget=forms.NumberInput(attrs={"class": "currency-input"}) ) @@ -333,13 +321,12 @@ def profile_page(request): is_success, is_failure, hint = get_session_hint(request, "profile_hint") context = dict( name=request.user.username, - balance=request.user.bankaccount.amount, - sign="-" if request.user.bankaccount.debit else "", + balance=request.user.bankaccount.balance, fail_message=is_failure, success_message=is_success, hint=hint, precision=settings.TALER_DIGITS, - currency=request.user.bankaccount.amount.currency, + currency=request.user.bankaccount.balance.amount.currency, account_no=request.user.bankaccount.account_no, wt_form=wtf, history=extract_history(request.user.bankaccount, -1 * (UINT64_MAX / 2 / 2)), @@ -423,16 +410,6 @@ def internal_register(request): user_account.save() bank_internal_account = BankAccount.objects.get(account_no=1) - # Raise: - # - # SameAccountException - # DebitLimitException - # CurrencyMismatch - # - # Amount group: - # BadFormatAmount - # NumberTooBig - # NegativeNumber wire_transfer( Amount(settings.TALER_CURRENCY, 100, 0), bank_internal_account, @@ -443,15 +420,13 @@ def internal_register(request): return user -## -# This method serves the request for programmatically -# registering a user. -# -# @param request Django-specific HTTP request object. -# @return Django-specific HTTP response object. @require_POST @csrf_exempt def register_headless(request): + """ + This method serves the request for programmatically + registering a user. + """ try: user = internal_register(request) @@ -465,15 +440,13 @@ def register_headless(request): return HttpResponse(status=200) -## -# This method serves the request for registering a user. -# If successful, it redirects the user to their profile page; -# otherwise it will show again the same form (currently, without -# displaying _any_ error/warning message.) -# -# @param request Django-specific HTTP request object. -# @return Django-specific HTTP response object. def register(request): + """ + This method serves the request for registering a user. + If successful, it redirects the user to their profile page; + otherwise it will show again the same form (currently, without + displaying _any_ error/warning message.) + """ if request.method != "POST": return render(request, "register.html") @@ -535,7 +508,7 @@ def logout_view(request): # @return the history array. def extract_history(account, delta, start=None): history = [] - qs = query_history(account, "both", delta, start, "descending") + qs = query_history(account, "both", delta, start) for item in qs: if item.credit_account == account: counterpart = item.debit_account @@ -589,7 +562,6 @@ def serve_public_accounts(request, name=None, page=None): # and django/python is not allowing slicing with big numbers. UINT64_MAX / 2 / 2, 0, - "descending", ).count() DELTA = 30 # '//' operator is NO floating point. @@ -708,7 +680,7 @@ def query_history_range(bank_account, direction, start, end, descending): # @param sign this value ("+"/"-") determines whether the history # entries will be younger / older than @a start. # @param ordering "descending" or anything else (for "ascending"). -def query_history(bank_account, direction, delta, start, ordering): +def query_history(bank_account, direction, delta, start): if start is None: if delta > 0: start = -1 @@ -722,7 +694,7 @@ def query_history(bank_account, direction, delta, start, ordering): qs = BankTransaction.objects.filter( direction_switch(bank_account, direction), sign_filter ) - order = "-id" if "descending" == ordering else "id" + order = "id" if (delta > 0) else "-id" return qs.order_by(order)[: abs(delta)] @@ -751,11 +723,11 @@ def build_history_response(qs, cancelled, user_account): history.append( dict( counterpart=counterpart, - amount=entry.amount.dump(), + amount=entry.amount.stringify(), sign=sign_, wt_subject=entry.subject, row_id=entry.id, - date=dict(t_ms=int(entry.date.timestamp())*1000) + date=dict(t_ms=int(entry.date.timestamp()) * 1000), ) ) return history @@ -786,8 +758,6 @@ def serve_history_range(request, user_account): history = build_history_response(qs, args.get("cancelled", "show"), user_account) - if not history: - return HttpResponse(status=204) return JsonResponse(dict(data=history), status=200) @@ -806,15 +776,13 @@ def serve_history(request, user_account): args.get("direction"), args.get("delta"), args.get("start", None), - args.get("ordering", "descending"), ) history = build_history_response(qs, args.get("cancelled", "show"), user_account) - if not history: - return HttpResponse(status=204) return JsonResponse(dict(data=history), status=200) + ## # Implements the HTTP basic auth schema. # @@ -830,64 +798,13 @@ def basic_auth(request): if len(tokens) != 2: raise LoginFailed("invalid Authorization header") - # decode the base64 content. + # decode the base64 content. if tokens[0] != "Basic": raise LoginFailed("Not supporting '%s' authorization method" % tokens[0]) username, password = base64.b64decode(tokens[1]).decode("utf-8").split(":") return django.contrib.auth.authenticate(username=username, password=password) -## -# Serve a request of /reject (for rejecting wire transfers). -# -# @param request Django-specific HTTP request object. -# @param user_account the account that is going to reject the -# transfer. Used to check whether they have this right -# or not (only accounts which _got_ payed can cancel the -# transaction.) -@transaction.atomic -@csrf_exempt -@require_http_methods(["PUT", "POST"]) -@login_via_headers -def reject(request, user_account): - - data = RejectData(json.loads(decode_body(request))) - - trans = BankTransaction.objects.get(id=data.get("row_id")) - if trans.credit_account.account_no != user_account.bankaccount.account_no: - raise RejectNoRightsException() - trans.cancelled = True - if trans.debit_account.debit: - # balance is negative - if 1 > Amount.cmp(trans.debit_account.amount, trans.amount): - # debit_account.amount <= trans.amount - trans.debit_account.debit = False - tmp = Amount(**trans.amount.dump()) - tmp.subtract(trans.debit_account.amount) - trans.debit_account.amount.set(**tmp.dump()) - else: - # debit_account > trans.amount - trans.debit_account.amount.subtract(trans.amount) - else: - # balance is positive, simply add - trans.debit_account.amount.add(trans.amount) - if trans.credit_account.debit: - # credit account balance is already negative - trans.credit_account.amount.add(trans.amount) - else: - if -1 == Amount.cmp(trans.credit_account.amount, trans.amount): - # credit_account.amount < trans.amount - trans.credit_account.debit = True - tmp = Amount(**trans.amount.dump()) - tmp.subtract(trans.credit_account.amount) - trans.credit_account.amount.set(**tmp.dump()) - else: - # credit_account.amount >= trans.amount - trans.credit_account.amount.subtract(trans.amount) - - trans.save() - return HttpResponse(status=204) - ## # Serve a request to make a wire transfer. Allows fintech @@ -906,17 +823,40 @@ def add_incoming(request, user_account): subject = "%s %s" % (data.get("subject"), data.get("exchange_url")) - credit_account = BankAccount.objects.get(account_no=data.get("credit_account")) + try: + credit_account = BankAccount.objects.get(account_no=data.get("credit_account")) + except BankAccount.DoesNotExist: + return JsonResponse( + { + "error": "Bank account not found" + }, + status=404, + ) + + + amount = Amount.parse(data.get("amount")) + + if amount.currency != settings.TALER_CURRENCY: + return JsonResponse( + { + "error": "Incorrect currency" + }, + status=400, + ) + wtrans = wire_transfer( - Amount.parse(data.get("amount")), + amount, user_account.bankaccount, credit_account, subject, ) return JsonResponse( - {"row_id": wtrans.id, "timestamp": dict(t_ms=(int(wtrans.date.timestamp())*1000))} + { + "row_id": wtrans.id, + "timestamp": dict(t_ms=(int(wtrans.date.timestamp()) * 1000)), + } ) @@ -927,10 +867,10 @@ def withdraw_headless_uri(request, user): data = WithdrawHeadlessUri(json.loads(decode_body(request))) amount = Amount.parse(data.get("amount")) user_account = BankAccount.objects.get(user=user) - debt_threshold = Amount.parse(settings.TALER_MAX_DEBT) - if not check_transfer_allowed( - user_account.amount, user_account.debit, debt_threshold, amount - ): + withdraw_amount = SignedAmount(True, amount) + debt_threshold = SignedAmount.parse(settings.TALER_MAX_DEBT) + user_balance = SignedAmount(not user_account.debit, user_account.amount) + if user_balance - amount < -debt_threshold: raise DebitLimitException( f"Aborting payment initiated by '{user_account.user.username}', debit limit crossed." ) @@ -941,15 +881,13 @@ def withdraw_headless_uri(request, user): return JsonResponse({"taler_withdraw_uri": taler_withdraw_uri,}) -## -# Serves a headless withdrawal request for the Taler protocol. -# -# @param request Django-specific HTTP request. -# @return Django-specific HTTP response object. @login_via_headers @csrf_exempt @require_POST def withdraw_headless(request, user): + """ + Serves a headless withdrawal request for the Taler protocol. + """ data = WithdrawHeadless(json.loads(decode_body(request))) sender_payto = "payto://x-taler-bank/%s/%d" % ( @@ -965,7 +903,11 @@ def withdraw_headless(request, user): else: exchange_accno = get_acct_from_payto(exchange_payto) - exchange_bankaccount = BankAccount.objects.get(account_no=exchange_accno) + try: + exchange_bankaccount = BankAccount.objects.get(account_no=exchange_accno) + except ObjectDoesNotExist: + err = dict(hint="Bank account not found") + return JsonResponse(err, status=404) wire_transfer( Amount.parse(data.get("amount")), @@ -977,10 +919,12 @@ def withdraw_headless(request, user): return JsonResponse(ret_obj) -# Endpoint used by the browser and wallet to check withdraw status and -# put in the exchange info. @csrf_exempt def api_withdraw_operation(request, withdraw_id): + """ + Endpoint used by the browser and wallet to check withdraw status and + put in the exchange info. + """ try: op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id) except ObjectDoesNotExist: @@ -1038,32 +982,20 @@ def api_withdraw_operation(request, withdraw_id): return JsonResponse(dict(error="only GET and POST are allowed"), status=305) -def check_transfer_allowed(balance, balance_is_debit, debt_limit, transfer_amount): - if balance_is_debit: - total_debt = Amount(**transfer_amount.dump()) - total_debt.add(balance) - return Amount.cmp(total_debt, debt_limit) <= 0 - max_transfer = Amount(**balance.dump()) - max_transfer.add(debt_limit) - return Amount.cmp(transfer_amount, max_transfer) <= 0 - - -## -# Serve a Taler withdrawal request; takes the amount chosen -# by the user, and builds a response to trigger the wallet into -# the withdrawal protocol -# -# @param request Django-specific HTTP request. -# @return Django-specific HTTP response object. @login_required @require_POST def start_withdrawal(request): + """ + Serve a Taler withdrawal request; takes the amount chosen + by the user, and builds a response to trigger the wallet into + the withdrawal protocol + """ user_account = BankAccount.objects.get(user=request.user) amount = Amount.parse(request.POST.get("kudos_amount", "not-given")) - debt_threshold = Amount.parse(settings.TALER_MAX_DEBT) - if not check_transfer_allowed( - user_account.amount, user_account.debit, debt_threshold, amount - ): + withdraw_amount = SignedAmount(True, amount) + debt_threshold = SignedAmount.parse(settings.TALER_MAX_DEBT) + user_balance = user_account.balance + if user_balance - withdraw_amount < -debt_threshold: raise DebitLimitException( f"Aborting payment initiated by '{user_account.user.username}', debit limit crossed." ) @@ -1147,17 +1079,12 @@ def confirm_withdrawal(request, withdraw_id): raise Exception("not reached") -## -# Make a wire transfer between two accounts (internal to the bank) -# -# @param amount (object type) how much money the wire transfer is worth. -# FIXME: a check about whether this value is zero is missing -# @param debit_account the account that gives money. -# @param credit_account the account that receives money. -# @return a @a BankTransaction object. def wire_transfer(amount, debit_account, credit_account, subject): + """ + Make a wire transfer between two accounts (internal to the bank) + """ LOGGER.debug( - "%s => %s, %s, %s" + "transfering %s => %s, %s, %s" % ( debit_account.account_no, credit_account.account_no, @@ -1176,45 +1103,19 @@ def wire_transfer(amount, debit_account, credit_account, subject): subject=subject, ) - if debit_account.debit: - debit_account.amount.add(amount) - - elif -1 == Amount.cmp(debit_account.amount, amount): - debit_account.debit = True - tmp = Amount(**amount.dump()) - tmp.subtract(debit_account.amount) - debit_account.amount.set(**tmp.dump()) - else: - debit_account.amount.subtract(amount) - - if not credit_account.debit: - credit_account.amount.add(amount) - elif Amount.cmp(amount, credit_account.amount) == 1: - credit_account.debit = False - tmp = Amount(**amount.dump()) - tmp.subtract(credit_account.amount) - credit_account.amount.set(**tmp.dump()) - else: - credit_account.amount.subtract(amount) - - # Check here if any account went beyond the allowed - # debit threshold. - - threshold = Amount.parse(settings.TALER_MAX_DEBT) if debit_account.user.username == "Bank": - threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK) - - LOGGER.info( - f"Account {debit_account.user.username} " - + f"(bal {debit_account.amount.stringify()}, thr {threshold.stringify()} requested transfer of" - + f"{threshold.stringify()} to account {credit_account.user.username}" - ) + threshold = -SignedAmount.parse(settings.TALER_MAX_DEBT_BANK) + else: + threshold = -SignedAmount.parse(settings.TALER_MAX_DEBT) - if Amount.cmp(debit_account.amount, threshold) > 0 and debit_account.debit: + if debit_account.balance - SignedAmount(True, amount) < threshold: raise DebitLimitException( f"Aborting payment initiated by '{debit_account.user.username}', debit limit crossed." ) + debit_account.balance -= SignedAmount(True, amount) + credit_account.balance += SignedAmount(True, amount) + with transaction.atomic(): debit_account.save() credit_account.save() |