gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-bank] 02/02: accounts API


From: gnunet
Subject: [taler-bank] 02/02: accounts API
Date: Mon, 17 Feb 2020 20:01:39 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository bank.

commit bda6379eb4ddbb355c8c1452b848049178f87598
Author: Florian Dold <address@hidden>
AuthorDate: Mon Feb 17 20:01:29 2020 +0100

    accounts API
---
 .../app/management/commands/add_bank_account.py    |   2 +-
 .../management/commands/changepassword_unsafe.py   |   6 +-
 talerbank/app/migrations/0001_initial.py           | 104 +++++++++++---
 talerbank/app/models.py                            |   3 +-
 talerbank/app/tests.py                             |   8 +-
 talerbank/app/urls.py                              |  50 ++++++-
 talerbank/app/views.py                             | 154 +++++++++++++++++++--
 talerbank/settings.py                              |   4 +-
 8 files changed, 283 insertions(+), 48 deletions(-)

diff --git a/talerbank/app/management/commands/add_bank_account.py 
b/talerbank/app/management/commands/add_bank_account.py
index b0e6513..cf01162 100644
--- a/talerbank/app/management/commands/add_bank_account.py
+++ b/talerbank/app/management/commands/add_bank_account.py
@@ -35,6 +35,7 @@ import uuid
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.INFO)
 
+
 class Command(BaseCommand):
     help = "Add bank accounts."
 
@@ -67,4 +68,3 @@ class Command(BaseCommand):
             )
         else:
             print(f"Bank account {accountname} already exists.")
-
diff --git a/talerbank/app/management/commands/changepassword_unsafe.py 
b/talerbank/app/management/commands/changepassword_unsafe.py
index d2f88d1..56a730f 100644
--- a/talerbank/app/management/commands/changepassword_unsafe.py
+++ b/talerbank/app/management/commands/changepassword_unsafe.py
@@ -35,6 +35,7 @@ import uuid
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.INFO)
 
+
 class Command(BaseCommand):
     help = "Add bank accounts."
 
@@ -59,10 +60,7 @@ class Command(BaseCommand):
             existing_user.set_password(password)
             existing_user.save()
         except User.DoesNotExist:
-            print(
-                f"Account {accountname} does not exist"
-            )
+            print(f"Account {accountname} does not exist")
             sys.exit(1)
         else:
             print(f"Password for {accountname} changed")
-
diff --git a/talerbank/app/migrations/0001_initial.py 
b/talerbank/app/migrations/0001_initial.py
index 73a5206..a25b205 100644
--- a/talerbank/app/migrations/0001_initial.py
+++ b/talerbank/app/migrations/0001_initial.py
@@ -17,37 +17,97 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.CreateModel(
-            name='BankAccount',
+            name="BankAccount",
             fields=[
-                ('is_public', models.BooleanField(default=False)),
-                ('account_no', models.AutoField(primary_key=True, 
serialize=False)),
-                ('balance', 
talerbank.app.models.SignedAmountField(default=talerbank.app.models.get_zero_signed_amount)),
-                ('user', 
models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, 
to=settings.AUTH_USER_MODEL)),
+                ("is_public", models.BooleanField(default=False)),
+                ("account_no", models.AutoField(primary_key=True, 
serialize=False)),
+                (
+                    "balance",
+                    talerbank.app.models.SignedAmountField(
+                        default=talerbank.app.models.get_zero_signed_amount
+                    ),
+                ),
+                (
+                    "user",
+                    models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='TalerWithdrawOperation',
+            name="TalerWithdrawOperation",
             fields=[
-                ('withdraw_id', models.UUIDField(default=uuid.uuid4, 
editable=False, primary_key=True, serialize=False)),
-                ('amount', talerbank.app.models.AmountField(default=False)),
-                ('selection_done', models.BooleanField(default=False)),
-                ('withdraw_done', models.BooleanField(default=False)),
-                ('selected_reserve_pub', models.TextField(null=True)),
-                ('selected_exchange_account', models.ForeignKey(null=True, 
on_delete=django.db.models.deletion.CASCADE, 
related_name='selected_exchange_account', to='app.BankAccount')),
-                ('withdraw_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='withdraw_account', to='app.BankAccount')),
+                (
+                    "withdraw_id",
+                    models.UUIDField(
+                        default=uuid.uuid4,
+                        editable=False,
+                        primary_key=True,
+                        serialize=False,
+                    ),
+                ),
+                ("amount", talerbank.app.models.AmountField(default=False)),
+                ("selection_done", models.BooleanField(default=False)),
+                ("confirmation_done", models.BooleanField(default=False)),
+                ("aborted", models.BooleanField(default=False)),
+                ("selected_reserve_pub", models.TextField(null=True)),
+                (
+                    "selected_exchange_account",
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="selected_exchange_account",
+                        to="app.BankAccount",
+                    ),
+                ),
+                (
+                    "withdraw_account",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="withdraw_account",
+                        to="app.BankAccount",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='BankTransaction',
+            name="BankTransaction",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
-                ('amount', talerbank.app.models.AmountField(default=False)),
-                ('subject', models.CharField(default='(no subject given)', 
max_length=200)),
-                ('date', models.DateTimeField(auto_now=True, db_index=True)),
-                ('cancelled', models.BooleanField(default=False)),
-                ('request_uid', models.CharField(max_length=128, unique=True)),
-                ('credit_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='credit_account', to='app.BankAccount')),
-                ('debit_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='debit_account', to='app.BankAccount')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("amount", talerbank.app.models.AmountField(default=False)),
+                (
+                    "subject",
+                    models.CharField(default="(no subject given)", 
max_length=200),
+                ),
+                ("date", models.DateTimeField(auto_now=True, db_index=True)),
+                ("cancelled", models.BooleanField(default=False)),
+                ("request_uid", models.CharField(max_length=128, unique=True)),
+                (
+                    "credit_account",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="credit_account",
+                        to="app.BankAccount",
+                    ),
+                ),
+                (
+                    "debit_account",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="debit_account",
+                        to="app.BankAccount",
+                    ),
+                ),
             ],
         ),
     ]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index 3833c22..5c4c9ac 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -205,7 +205,8 @@ class TalerWithdrawOperation(models.Model):
         related_name="withdraw_account",
     )
     selection_done = models.BooleanField(default=False)
-    withdraw_done = models.BooleanField(default=False)
+    confirmation_done = models.BooleanField(default=False)
+    aborted = models.BooleanField(default=False)
     selected_exchange_account = models.ForeignKey(
         BankAccount,
         null=True,
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index ad792b7..abbf9f3 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -385,9 +385,10 @@ class AddIncomingTestCase(TestCase):
     def test_add_incoming(self):
         client = Client()
         request_body = dict(
-                reserve_pub="TESTWTID",
-                amount=f"{settings.TALER_CURRENCY}:1.0",
-                debit_account="payto://x-taler-bank/bank_user")
+            reserve_pub="TESTWTID",
+            amount=f"{settings.TALER_CURRENCY}:1.0",
+            debit_account="payto://x-taler-bank/bank_user",
+        )
         response = client.post(
             reverse("twg-add-incoming", urlconf=urls, args=["user_user"]),
             data=json.dumps(request_body),
@@ -430,7 +431,6 @@ class HistoryTestCase(TestCase):
         clear_db()
 
     def test_history(self):
-
         def histquery(**urlargs):
             response = self.client.get(
                 reverse("history", urlconf=urls),
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index e929125..c2a524c 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -23,23 +23,63 @@ from django.views.generic.base import RedirectView
 from django.contrib.auth import views as auth_views
 from . import views
 
+# These paths are part of the GNU Taler wire gatweay API
 taler_wire_gateway_patterns = [
     path("<str:acct_id>/", views.twg_base, name="twg-base"),
-    path("<str:acct_id>/admin/add-incoming", views.twg_add_incoming, 
name="twg-add-incoming"),
-    path("<str:acct_id>/history/incoming", views.twg_history_incoming, 
name="twg-history-incoming"),
-    path("<str:acct_id>/history/outgoing", views.twg_history_outgoing, 
name="twg-history-outgoing"),
+    path(
+        "<str:acct_id>/admin/add-incoming",
+        views.twg_add_incoming,
+        name="twg-add-incoming",
+    ),
+    path(
+        "<str:acct_id>/history/incoming",
+        views.twg_history_incoming,
+        name="twg-history-incoming",
+    ),
+    path(
+        "<str:acct_id>/history/outgoing",
+        views.twg_history_outgoing,
+        name="twg-history-outgoing",
+    ),
     path("<str:acct_id>/transfer", views.twg_transfer, name="twg-transfer"),
 ]
 
+# These paths are part of the bank integration API
 taler_bank_api_patterns = [
+    path(
+        "withdrawal-operation/<str:withdraw_id>",
+        views.register_headless,
+        name="tba-withdrawal-operation",
+    ),
+]
+
+taler_bank_accounts_api_patterns = [
+    path("accounts/<str:acct_id>/balance", views.bank_accounts_api_balance),
+    path(
+        "accounts/<str:acct_id>/withdrawals", 
views.bank_accounts_api_create_withdrawal
+    ),
+    path(
+        "accounts/<str:acct_id>/withdrawals/<str:wid>",
+        views.bank_accounts_api_get_withdrawal,
+    ),
+    path(
+        "accounts/<str:acct_id>/withdrawals/<str:wid>/confirm",
+        views.bank_accounts_api_confirm_withdrawal,
+    ),
+    path(
+        "accounts/<str:acct_id>/withdrawals/<str:wid>/abort",
+        views.bank_accounts_api_abort_withdrawal,
+    ),
     path("testing/withdraw", views.withdraw_headless, name="testing-withdraw"),
-    path("testing/withdraw-uri", views.withdraw_headless_uri, 
name="testing-withdraw-uri"),
+    path(
+        "testing/withdraw-uri", views.withdraw_headless_uri, 
name="testing-withdraw-uri"
+    ),
     path("testing/register", views.register_headless, 
name="testing-withdraw-register"),
-    path("withdrawal-operation/<str:withdraw_id>", views.register_headless, 
name="tba-withdrawal-operation"),
 ]
 
 urlpatterns = [
     path("taler-bank-api/", include(taler_bank_api_patterns)),
+    path("", include(taler_bank_accounts_api_patterns)),
     path("taler-wire-gateway/", include(taler_wire_gateway_patterns)),
     path("", RedirectView.as_view(pattern_name="profile"), name="index"),
     path("favicon.ico", views.ignore),
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index 16a4b06..2e74f8f 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -408,12 +408,13 @@ def internal_register(request):
 
     # Registration goes through.
     with transaction.atomic():
+        bank_internal_account = BankAccount.objects.get(account_no=1)
+
         user = User.objects.create_user(username=username, password=password)
         user_account = BankAccount(user=user)
         user_account.save()
 
         # Give the user their joining bonus
-        bank_internal_account = BankAccount.objects.get(account_no=1)
         wire_transfer(
             Amount(settings.TALER_CURRENCY, 100, 0),
             bank_internal_account,
@@ -835,7 +836,9 @@ def twg_transfer(request, user_account, acct_id):
 
     subject = f"{wtid} {exchange_base_url}"
 
-    wtrans = wire_transfer(amount, exchange_account, credit_account, subject, 
request_uid)
+    wtrans = wire_transfer(
+        amount, exchange_account, credit_account, subject, request_uid
+    )
 
     return JsonResponse(
         {
@@ -1015,7 +1018,7 @@ def api_withdraw_operation(request, withdraw_id):
         selected_reserve_pub = data.get("reserve_pub")
         if not isinstance(selected_reserve_pub, str):
             return JsonResponse(dict(error="reserve_pub must be a string"), 
status=400)
-        if op.selection_done or op.withdraw_done:
+        if op.selection_done or op.confirmation_done:
             if (
                 op.selected_exchange_account != exchange_account
                 or op.selected_reserve_pub != selected_reserve_pub
@@ -1026,17 +1029,26 @@ def api_withdraw_operation(request, withdraw_id):
                 )
             # No conflict, same data!
             return JsonResponse(dict(), status=200)
-        op.selected_exchange_account = exchange_account
-        op.selected_reserve_pub = selected_reserve_pub
-        op.selection_done = True
-        op.save()
+        with transaction.atomic():
+            op.selected_exchange_account = exchange_account
+            op.selected_reserve_pub = selected_reserve_pub
+            if op.confirmation_done and not op.selection_done:
+                # Confirmation already happened, we still need to transfer 
funds!
+                wire_transfer(
+                    op.amount,
+                    user_account,
+                    op.selected_exchange_account,
+                    op.selected_reserve_pub,
+                )
+            op.selection_done = True
+            op.save()
         return JsonResponse(dict(), status=200)
     elif request.method == "GET":
         host = request.get_host()
         return JsonResponse(
             dict(
                 selection_done=op.selection_done,
-                transfer_done=op.withdraw_done,
+                transfer_done=op.confirmation_done,
                 amount=op.amount.stringify(),
                 wire_types=["x-taler-bank"],
                 
sender_wire=f"payto://x-taler-bank/{host}/{op.withdraw_account.user.username}",
@@ -1106,7 +1118,7 @@ def confirm_withdrawal(request, withdraw_id):
     op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id)
     if not op.selection_done:
         raise Exception("invalid state (withdrawal parameter selection not 
done)")
-    if op.withdraw_done:
+    if op.confirmation_done:
         return redirect("profile")
     if request.method == "POST":
         hashed_attempt = hash_answer(request.POST.get("pin_0", ""))
@@ -1119,7 +1131,7 @@ def confirm_withdrawal(request, withdraw_id):
             )
             request.session["captcha_failed"] = True, False, "Wrong CAPTCHA 
answer."
             return redirect("withdraw-confirm", withdraw_id=withdraw_id)
-        op.withdraw_done = True
+        op.confirmation_done = True
         op.save()
         wire_transfer(
             op.amount,
@@ -1218,3 +1230,125 @@ def wire_transfer(amount, debit_account, 
credit_account, subject, request_uid=No
         transaction_item.save()
 
     return transaction_item
+
+
+@csrf_exempt
+@require_GET
+@login_via_headers
+def bank_accounts_api_balance(request, user_account, acct_id):
+    """
+    Query the balance for an account.
+    """
+    acct = user_account.bankaccount
+
+    if acct_id != user_account.username:
+        # FIXME: respond nicely
+        raise Exception(
+            f"credentials do not match URL ('{acct_id}' vs 
'{user_account.username}')"
+        )
+
+    return JsonResponse(dict(balance=acct.balance.stringify()))
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_create_withdrawal(request, user, acct_id):
+    user_account = BankAccount.objects.get(user=user)
+
+    if acct_id != user_account.user.username:
+        # FIXME: respond nicely
+        raise Exception(
+            f"credentials do not match URL ('{acct_id}' vs 
'{user_account.username}')"
+        )
+
+    data = WithdrawHeadlessUri(json.loads(decode_body(request)))
+    amount = Amount.parse(data.get("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 {debt_threshold} crossed."
+        )
+    op = TalerWithdrawOperation(amount=amount, withdraw_account=user_account)
+    op.save()
+    host = request.get_host()
+    taler_withdraw_uri = f"taler://withdraw/{host}/-/{op.withdraw_id}"
+    return JsonResponse(
+        {"taler_withdraw_uri": taler_withdraw_uri, "withdrawal_id": 
op.withdraw_id}
+    )
+
+
+@csrf_exempt
+@require_GET
+@login_via_headers
+def bank_accounts_api_get_withdrawal(request, user, acct_id, wid):
+    user_account = BankAccount.objects.get(user=user)
+    if acct_id != user_account.user.username:
+        # FIXME: respond nicely
+        raise Exception(
+            f"credentials do not match URL ('{acct_id}' vs 
'{user_account.username}')"
+        )
+    op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+    selected_exchange_account = None
+    if op.selected_exchange_account:
+        selected_exchange_account = op.selected_exchange_account.user.name
+    return JsonResponse(
+        {
+            "amount": op.amount.stringify(),
+            "selection_done": op.selection_done,
+            "confirmation_done": op.confirmation_done,
+            "selected_reserve_pub": op.selected_reserve_pub,
+            "selected_exchange_account": selected_exchange_account,
+            "aborted": op.aborted,
+        }
+    )
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_abort_withdrawal(request, user, acct_id, wid):
+    user_account = BankAccount.objects.get(user=user)
+    if acct_id != user_account.user.username:
+        # FIXME: respond nicely
+        raise Exception(
+            f"credentials do not match URL ('{acct_id}' vs 
'{user_account.username}')"
+        )
+    op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+
+    if op.confirmation_done:
+        return JsonResponse(dict(hint="can't abort confirmed withdrawal"), 
status=409)
+    op.aborted = True
+    op.save()
+    return JsonResponse(dict(), status=200)
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_confirm_withdrawal(request, user, acct_id, wid):
+    user_account = BankAccount.objects.get(user=user)
+    if acct_id != user_account.user.username:
+        # FIXME: respond nicely
+        raise Exception(
+            f"credentials do not match URL ('{acct_id}' vs 
'{user_account.username}')"
+        )
+    op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+    if op.confirmation_done:
+        return JsonResponse(dict(), status=200)
+    if op.aborted:
+        return JsonResponse(dict(hint="can't confirm aborted withdrawal"), 
status=409)
+
+    with transaction.atomic():
+        if op.selection_done:
+            wire_transfer(
+                op.amount,
+                user_account,
+                op.selected_exchange_account,
+                op.selected_reserve_pub,
+            )
+        op.confirmation_done = True
+        op.save()
+    return JsonResponse(dict(), status=200)
diff --git a/talerbank/settings.py b/talerbank/settings.py
index ba880e3..8024907 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -217,7 +217,9 @@ else:
     ALLOW_REGISTRATIONS = False
 
 
-_show_freeform_withdrawal = TC.value_string("bank", 
"SHOW_FREEFORM_WITHDRAWAL", default="no")
+_show_freeform_withdrawal = TC.value_string(
+    "bank", "SHOW_FREEFORM_WITHDRAWAL", default="no"
+)
 if _show_freeform_withdrawal.lower() == "yes":
     SHOW_FREEFORM_WITHDRAWAL = True
 else:

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]