summaryrefslogtreecommitdiff
path: root/talerbank/app/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'talerbank/app/views.py')
-rw-r--r--talerbank/app/views.py273
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()