[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[GNUnet-SVN] [taler-bank] 04/04: porting the bank to use the new Amount
From: |
gnunet |
Subject: |
[GNUnet-SVN] [taler-bank] 04/04: porting the bank to use the new Amount object |
Date: |
Tue, 31 Oct 2017 15:05:11 +0100 |
This is an automated email from the git hooks/post-receive script.
marcello pushed a commit to branch master
in repository bank.
commit fc6a56f276dfb2fba4d4acbe756c22492430439b
Author: Marcello Stanisci <address@hidden>
AuthorDate: Tue Oct 31 15:04:33 2017 +0100
porting the bank to use the new Amount object
---
bank-check.conf | 4 +-
talerbank/app/Makefile.am | 2 +-
talerbank/app/amount.py | 131 +++++++++++++++
talerbank/app/amounts.py | 101 ------------
talerbank/app/management/commands/dump_talerdb.py | 2 +-
.../app/migrations/0002_bankaccount_amount.py | 21 +++
.../app/migrations/0003_auto_20171030_1346.py | 21 +++
.../app/migrations/0004_auto_20171030_1428.py | 34 ++++
.../0005_remove_banktransaction_currency.py | 19 +++
.../app/migrations/0006_auto_20171031_0823.py | 21 +++
.../app/migrations/0007_auto_20171031_0906.py | 21 +++
.../app/migrations/0008_auto_20171031_0938.py | 31 ++++
talerbank/app/models.py | 48 +-----
talerbank/app/templates/pin_tan.html | 2 +-
talerbank/app/templates/profile_page.html | 2 +-
talerbank/app/templates/public_accounts.html | 2 +-
talerbank/app/tests.py | 142 ++++++++--------
talerbank/app/tests_alt.py | 10 +-
talerbank/app/views.py | 178 +++++++++------------
talerbank/settings.py | 4 +-
20 files changed, 476 insertions(+), 320 deletions(-)
diff --git a/bank-check.conf b/bank-check.conf
index b02bf28..46658e2 100644
--- a/bank-check.conf
+++ b/bank-check.conf
@@ -10,7 +10,7 @@ DATABASE = postgres:///talercheck
NDIGITS = 2
# FIXME
-MAX_DEBT = KUDOS:50
+MAX_DEBT = KUDOS:50.0
# FIXME
-MAX_DEBT_BANK = KUDOS:0
+MAX_DEBT_BANK = KUDOS:0.0
diff --git a/talerbank/app/Makefile.am b/talerbank/app/Makefile.am
index 6376a68..b3efdd7 100644
--- a/talerbank/app/Makefile.am
+++ b/talerbank/app/Makefile.am
@@ -5,7 +5,7 @@ EXTRA_DIST = \
schemas.py \
urls.py \
views.py \
- amounts.py \
+ amount.py \
checks.py \
__init__.py \
models.py \
diff --git a/talerbank/app/amount.py b/talerbank/app/amount.py
new file mode 100644
index 0000000..c66691a
--- /dev/null
+++ b/talerbank/app/amount.py
@@ -0,0 +1,131 @@
+# This file is part of TALER
+# (C) 2017 TALER SYSTEMS
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
+#
+# @author Marcello Stanisci
+# @version 0.0
+# @repository https://git.taler.net/copylib.git/
+# This code is "copylib", it is versioned under the Git repository
+# mentioned above, and it is meant to be manually copied into any project
+# which might need it.
+
+class CurrencyMismatch(Exception):
+ pass
+
+class BadFormatAmount(Exception):
+ def __init__(self, faulty_str):
+ self.faulty_str = faulty_str
+
+class Amount:
+ # How many "fraction" units make one "value" unit of currency
+ # (Taler requires 10^8). Do not change this 'constant'.
+ @staticmethod
+ def FRACTION():
+ return 10 ** 8
+
+ @staticmethod
+ def MAX_VALUE():
+ return (2 ** 53) - 1
+
+ def __init__(self, currency, value=0, fraction=0):
+ # type: (str, int, int) -> Amount
+ assert(value >= 0 and fraction >= 0)
+ self.value = value
+ self.fraction = fraction
+ self.currency = currency
+ self.__normalize()
+ assert(self.value <= Amount.MAX_VALUE())
+
+ # Normalize amount
+ def __normalize(self):
+ if self.fraction >= Amount.FRACTION():
+ self.value += int(self.fraction / Amount.FRACTION())
+ self.fraction = self.fraction % Amount.FRACTION()
+
+ # Parse a string matching the format "A:B.C"
+ # instantiating an amount object.
+ @classmethod
+ def parse(cls, amount_str):
+ exp = '^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$'
+ import re
+ parsed = re.search(exp, amount_str)
+ if not parsed:
+ raise BadFormatAmount(amount_str)
+ value = int(parsed.group(2))
+ fraction = 0
+ for i, digit in enumerate(parsed.group(3)):
+ fraction += int(int(digit) * (Amount.FRACTION() / 10 ** (i+1)))
+ return cls(parsed.group(1), value, fraction)
+
+ # Comare two amounts, return:
+ # -1 if a < b
+ # 0 if a == b
+ # 1 if a > b
+ @staticmethod
+ def cmp(a, b):
+ if a.currency != b.currency:
+ raise CurrencyMismatch()
+ if a.value == b.value:
+ if a.fraction < b.fraction:
+ return -1
+ if a.fraction > b.fraction:
+ return 1
+ return 0
+ if a.value < b.value:
+ return -1
+ return 1
+
+ def set(self, currency, value=0, fraction=0):
+ self.currency = currency
+ self.value = value
+ self.fraction = fraction
+
+ # Add the given amount to this one
+ def add(self, a):
+ if self.currency != a.currency:
+ raise CurrencyMismatch()
+ self.value += a.value
+ self.fraction += a.fraction
+ self.__normalize()
+
+ # Subtract passed amount from this one
+ def subtract(self, a):
+ if self.currency != a.currency:
+ raise CurrencyMismatch()
+ if self.fraction < a.fraction:
+ self.fraction += Amount.FRACTION()
+ self.value -= 1
+ if self.value < a.value:
+ raise ValueError('self is lesser than amount to be subtracted')
+ self.value -= a.value
+ self.fraction -= a.fraction
+
+ # Dump string from this amount, will put 'ndigits' numbers
+ # after the dot.
+ def stringify(self, ndigits):
+ assert ndigits > 0
+ ret = '%s:%s.' % (self.currency, str(self.value))
+ f = self.fraction
+ for i in range(0, ndigits):
+ ret += str(int(f / (Amount.FRACTION() / 10)))
+ f = (f * 10) % (Amount.FRACTION())
+ return ret
+
+ # Dump the Taler-compliant 'dict' amount
+ def dump(self):
+ return dict(value=self.value,
+ fraction=self.fraction,
+ currency=self.currency)
diff --git a/talerbank/app/amounts.py b/talerbank/app/amounts.py
deleted file mode 100644
index f9bdd02..0000000
--- a/talerbank/app/amounts.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# This file is part of TALER
-# (C) 2016 INRIA
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Affero General Public License as published by the Free
Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-# @author Marcello Stanisci
-# @author Florian Dold
-
-
-import re
-import math
-import logging
-from django.conf import settings
-
-logger = logging.getLogger(__name__)
-
-FRACTION = 100000000
-
-class CurrencyMismatchException(Exception):
- def __init__(self, msg=None, status_code=0):
- self.msg = msg
- # HTTP status code to be returned as response for
- # this exception
- self.status_code = status_code
-
-class BadFormatAmount(Exception):
- def __init__(self, msg=None, status_code=0):
- self.msg = msg
- # HTTP status code to be returned as response for
- # this exception
- self.status_code = status_code
-
-
-def check_currency(a1, a2):
- if a1["currency"] != a2["currency"]:
- logger.error("Different currencies given: %s vs %s" % (a1["currency"],
a2["currency"]))
- raise CurrencyMismatchException
-
-def get_zero():
- return dict(value=0, fraction=0, currency=settings.TALER_CURRENCY)
-
-def amount_add(a1, a2):
- check_currency(a1, a2)
- a1_float = floatify(a1)
- a2_float = floatify(a2)
- return parse_amount("%s:%s" % (a2["currency"], str(a1_float + a2_float)))
-
-def amount_sub(a1, a2):
- check_currency(a1, a2)
- a1_float = floatify(a1)
- a2_float = floatify(a2)
- sub = a1_float - a2_float
- fmt = "%s:%s" % (a2["currency"], str(sub))
- return parse_amount(fmt)
-
-# Return -1 if a1 < a2, 0 if a1 == a2, 1 if a1 > a2
-def amount_cmp(a1, a2):
- check_currency(a1, a2)
- a1_float = floatify(a1)
- a2_float = floatify(a2)
-
- if a1_float < a2_float:
- return -1
- elif a1_float == a2_float:
- return 0
-
- return 1
-
-
-def floatify(amount_dict):
- return amount_dict['value'] + (float(amount_dict['fraction']) /
float(FRACTION))
-
-def stringify(amount_float, digits=2):
- o = "".join(["%.", "%sf" % digits])
- return o % amount_float
-
-def parse_amount(amount_str):
- """
- Parse amount of return None if not a
- valid amount string
- """
- parsed = re.search("^\s*([-_*A-Za-z0-9]+):([0-9]+)(\.[0-9]+)?\s*$",
amount_str)
- if not parsed:
- raise BadFormatAmount
- value = int(parsed.group(2))
- fraction = 0
- if parsed.group(3) is not None:
- for i, digit in enumerate(parsed.group(3)[1:]):
- fraction += int(int(digit) * (FRACTION / 10 ** (i+1)))
- return {'value': value,
- 'fraction': fraction,
- 'currency': parsed.group(1)}
diff --git a/talerbank/app/management/commands/dump_talerdb.py
b/talerbank/app/management/commands/dump_talerdb.py
index ab587d9..b4a93fc 100644
--- a/talerbank/app/management/commands/dump_talerdb.py
+++ b/talerbank/app/management/commands/dump_talerdb.py
@@ -50,7 +50,7 @@ def dump_history():
# as the first/last character on a line makes flake8 complain
msg.append("+%s, " % item.credit_account.account_no)
msg.append("-%s, " % item.debit_account.account_no)
- msg.append("%.2f, " % floatify(item.amount_obj))
+ msg.append(item.amount.stringify(2))
msg.append(item.subject)
print(''.join(msg))
except (OperationalError, ProgrammingError):
diff --git a/talerbank/app/migrations/0002_bankaccount_amount.py
b/talerbank/app/migrations/0002_bankaccount_amount.py
new file mode 100644
index 0000000..beaa1d8
--- /dev/null
+++ b/talerbank/app/migrations/0002_bankaccount_amount.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 13:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bankaccount',
+ name='amount',
+ field=talerbank.app.models.AmountField(default=False),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0003_auto_20171030_1346.py
b/talerbank/app/migrations/0003_auto_20171030_1346.py
new file mode 100644
index 0000000..91c6cb9
--- /dev/null
+++ b/talerbank/app/migrations/0003_auto_20171030_1346.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 13:46
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0002_bankaccount_amount'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='bankaccount',
+ name='amount',
+ field=talerbank.app.models.AmountField(),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0004_auto_20171030_1428.py
b/talerbank/app/migrations/0004_auto_20171030_1428.py
new file mode 100644
index 0000000..b93ebd4
--- /dev/null
+++ b/talerbank/app/migrations/0004_auto_20171030_1428.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 14:28
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0003_auto_20171030_1346'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='banktransaction',
+ name='amount_fraction',
+ ),
+ migrations.RemoveField(
+ model_name='banktransaction',
+ name='amount_value',
+ ),
+ migrations.AddField(
+ model_name='banktransaction',
+ name='amount',
+ field=talerbank.app.models.AmountField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='bankaccount',
+ name='amount',
+ field=talerbank.app.models.AmountField(default=False),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0005_remove_banktransaction_currency.py
b/talerbank/app/migrations/0005_remove_banktransaction_currency.py
new file mode 100644
index 0000000..9cd781f
--- /dev/null
+++ b/talerbank/app/migrations/0005_remove_banktransaction_currency.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 14:37
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0004_auto_20171030_1428'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='banktransaction',
+ name='currency',
+ ),
+ ]
diff --git a/talerbank/app/migrations/0006_auto_20171031_0823.py
b/talerbank/app/migrations/0006_auto_20171031_0823.py
new file mode 100644
index 0000000..67c1a70
--- /dev/null
+++ b/talerbank/app/migrations/0006_auto_20171031_0823.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 08:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0005_remove_banktransaction_currency'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='bankaccount',
+ name='amount',
+ field=talerbank.app.models.AmountField(default=None),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0007_auto_20171031_0906.py
b/talerbank/app/migrations/0007_auto_20171031_0906.py
new file mode 100644
index 0000000..923cff2
--- /dev/null
+++ b/talerbank/app/migrations/0007_auto_20171031_0906.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 09:06
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0006_auto_20171031_0823'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='bankaccount',
+ name='amount',
+
field=talerbank.app.models.AmountField(default=talerbank.app.models.get_zero_amount),
+ ),
+ ]
diff --git a/talerbank/app/migrations/0008_auto_20171031_0938.py
b/talerbank/app/migrations/0008_auto_20171031_0938.py
new file mode 100644
index 0000000..3b97829
--- /dev/null
+++ b/talerbank/app/migrations/0008_auto_20171031_0938.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 09:38
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0007_auto_20171031_0906'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='bankaccount',
+ name='balance',
+ ),
+ migrations.RemoveField(
+ model_name='bankaccount',
+ name='balance_fraction',
+ ),
+ migrations.RemoveField(
+ model_name='bankaccount',
+ name='balance_value',
+ ),
+ migrations.RemoveField(
+ model_name='bankaccount',
+ name='currency',
+ ),
+ ]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index cbb47e0..ebbb9f0 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -43,54 +43,31 @@ class AmountField(models.Field):
def from_db_value(self, value, expression, connection, context):
if None is value:
- return value
+ return amount.Amount.parse(settings.TALER_CURRENCY)
return amount.Amount.parse(value)
def to_python(self, value):
- if isinstance(value, amount.Amount) or None is value:
+ if isinstance(value, amount.Amount):
return value
try:
+ if None is value:
+ return amount.Amount.parse(settings.TALER_CURRENCY)
return amount.Amount.parse(value)
except amount.BadAmount:
raise models.ValidationError()
+def get_zero_amount():
+ return amount.Amount(settings.TALER_CURRENCY)
class BankAccount(models.Model):
is_public = models.BooleanField(default=False)
- # Handier than keeping the amount signed, for two reasons:
- # (1) checking if someone is in debt is less verbose: with signed
- # amounts we have to check if the amount is less than zero; this
- # way we only check if a boolean is true. (2) The bank logic is
- # ready to welcome a data type for amounts which doesn't have any
- # sign notion, like Taler amounts do.
debit = models.BooleanField(default=False)
- balance_value = models.IntegerField(default=0)
- balance_fraction = models.IntegerField(default=0)
- # From today's (16/10/2017) Mumble talk, it emerged that bank shouldn't
- # store amounts as floats, but: normal banks should not care about
- # Taler when representing values around in their databases..
- balance = models.FloatField(default=0)
- currency = models.CharField(max_length=12, default="")
account_no = models.AutoField(primary_key=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
-
- # EXPERIMENTAL CODE
- amount = AmountField()
-
- def _get_balance(self):
- return dict(value=self.balance_value,
- fraction=self.balance_fraction,
- currency=self.currency)
- def _set_balance(self, amount):
- self.balance_value = amount["value"]
- self.balance_fraction = amount["fraction"]
- self.currency = amount["currency"]
- balance_obj = property(_get_balance, _set_balance)
+ amount = AmountField(default=get_zero_amount)
class BankTransaction(models.Model):
- amount_value = models.IntegerField(default=0)
- amount_fraction = models.IntegerField(default=0)
- currency = models.CharField(max_length=12)
+ amount = AmountField(default=False)
debit_account = models.ForeignKey(BankAccount,
on_delete=models.CASCADE,
related_name="debit_account")
@@ -99,12 +76,3 @@ class BankTransaction(models.Model):
related_name="credit_account")
subject = models.CharField(default="(no subject given)", max_length=200)
date = models.DateTimeField(auto_now=True)
- def _get_amount(self):
- return dict(value=self.amount_value,
- fraction=self.amount_fraction,
- currency=self.currency)
- def _set_amount(self, amount):
- self.amount_value = amount["value"]
- self.amount_fraction = amount["fraction"]
- self.currency = amount["currency"]
- amount_obj = property(_get_amount, _set_amount)
diff --git a/talerbank/app/templates/pin_tan.html
b/talerbank/app/templates/pin_tan.html
index b545524..0855b3b 100644
--- a/talerbank/app/templates/pin_tan.html
+++ b/talerbank/app/templates/pin_tan.html
@@ -31,7 +31,7 @@
{% endif %}
<p>
{{ settings_value("TALER_CURRENCY") }} Bank needs to verify that you
- intend to withdraw <b>{{ amount }} {{ settings_value("TALER_CURRENCY")
}}</b> from
+ intend to withdraw <b>{{ amount }}</b> from
<b>{{ exchange }}</b>.
To prove that you are the account owner, please answer the
following "security question" (*):
diff --git a/talerbank/app/templates/profile_page.html
b/talerbank/app/templates/profile_page.html
index c64f215..ad8345d 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -125,7 +125,7 @@
<tr>
<td style="text-align:right">{{ item.date }}</td>
<td style="text-align:right">
- {{ item.float_amount }} {{ item.float_currency }}
+ {{ item.sign }} {{ item.amount }}
</td>
<td class="text-align:left">{% if item.counterpart_username %} {{
item.counterpart_username }} {% endif %} (account #{{ item.counterpart }})</td>
<td class="text-align:left">{{ item.subject }}</td>
diff --git a/talerbank/app/templates/public_accounts.html
b/talerbank/app/templates/public_accounts.html
index 2f38489..3c5b53e 100644
--- a/talerbank/app/templates/public_accounts.html
+++ b/talerbank/app/templates/public_accounts.html
@@ -54,7 +54,7 @@
<tr>
<td>{{entry.date}}</td>
<td>
- {{ entry.float_amount }} {{ entry.float_currency }}
+ {{ sign }} {{ entry.amount }}
</td>
<td>{% if entry.counterpart_username %} {{
entry.counterpart_username }} {% endif %} (account #{{ entry.counterpart
}})</td>
<td>
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index feff213..435fd7d 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -20,9 +20,9 @@ from django.conf import settings
from django.contrib.auth.models import User
from .models import BankAccount, BankTransaction
from . import urls
-from . import amounts
from .views import wire_transfer
import json
+from .amount import Amount, CurrencyMismatch, BadFormatAmount
import logging
@@ -39,7 +39,7 @@ class RegisterTestCase(TestCase):
def setUp(self):
bank = User.objects.create_user(username='Bank')
- ba = BankAccount(user=bank, currency=settings.TALER_CURRENCY)
+ ba = BankAccount(user=bank)
ba.account_no = 1
ba.save()
@@ -62,8 +62,8 @@ class RegisterWrongCurrencyTestCase(TestCase):
# Activating this user with a faulty currency.
def setUp(self):
- bank = User.objects.create_user(username='Bank')
- ba = BankAccount(user=bank, currency="XYZ")
+ ba = BankAccount(user=User.objects.create_user(username='Bank'),
+ amount=Amount('WRONGCURRENCY'))
ba.account_no = 1
ba.save()
@@ -88,8 +88,7 @@ class LoginTestCase(TestCase):
def setUp(self):
user = User.objects.create_user(username="test_user",
password="test_password")
- user_account = BankAccount(user=user,
- currency=settings.TALER_CURRENCY)
+ user_account = BankAccount(user=user)
user_account.save()
def tearDown(self):
@@ -116,20 +115,21 @@ class LoginTestCase(TestCase):
class AmountTestCase(TestCase):
def test_cmp(self):
- a1 = dict(value=1, fraction=0, currency="X")
- _a1 = dict(value=1, fraction=0, currency="X")
- a2 = dict(value=2, fraction=0, currency="X")
- self.assertEqual(-1, amounts.amount_cmp(a1, a2))
- self.assertEqual(1, amounts.amount_cmp(a2, a1))
- self.assertEqual(0, amounts.amount_cmp(a1, _a1))
+ a1 = Amount("X", 1)
+ _a1 = Amount("X", 1)
+ a2 = Amount("X", 2)
+
+ self.assertEqual(-1, Amount.cmp(a1, a2))
+ self.assertEqual(1, Amount.cmp(a2, a1))
+ self.assertEqual(0, Amount.cmp(a1, _a1))
# Trying to compare amount of different currencies
def test_cmp_diff_curr(self):
- a1 = dict(value=1, fraction=0, currency="X")
- a2 = dict(value=2, fraction=0, currency="Y")
+ a1 = Amount("X", 1)
+ a2 = Amount("Y", 2)
try:
- amounts.amount_cmp(a1, a2)
- except amounts.CurrencyMismatchException:
+ Amount.cmp(a1, a2)
+ except CurrencyMismatch:
self.assertTrue(True)
return
# Should never get here
@@ -140,14 +140,12 @@ class AddIncomingTestCase(TestCase):
"""Test money transfer's API"""
def setUp(self):
- bank = User.objects.create_user(username="bank_user",
- password="bank_password")
- bank_account = BankAccount(user=bank,
- currency=settings.TALER_CURRENCY)
- user = User.objects.create_user(username="user_user",
- password="user_password")
- user_account = BankAccount(user=user,
- currency=settings.TALER_CURRENCY)
+ bank_account = BankAccount(user=User.objects.create_user(
+ username="bank_user",
+ password="bank_password"))
+ user_account = BankAccount(user=User.objects.create_user(
+ username="user_user",
+ password="user_password"))
bank_account.save()
user_account.save()
@@ -206,22 +204,22 @@ class HistoryTestCase(TestCase):
def setUp(self):
user = User.objects.create_user(username='User', password="Password")
- ub = BankAccount(user=user, currency=settings.TALER_CURRENCY)
+ ub = BankAccount(user=user, amount=Amount(settings.TALER_CURRENCY,
100))
ub.account_no = 1
- ub.balance_obj = dict(value=100, fraction=0,
currency=settings.TALER_CURRENCY)
ub.save()
user_passive = User.objects.create_user(username='UserP',
password="PasswordP")
- ub_p = BankAccount(user=user_passive, currency=settings.TALER_CURRENCY)
+ ub_p = BankAccount(user=user_passive)
ub_p.account_no = 2
ub_p.save()
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="a")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="b")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="c")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="d")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="e")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="f")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="g")
- wire_transfer(dict(value=1, fraction=0,
currency=settings.TALER_CURRENCY), ub, ub_p, subject="h")
+ one = Amount(settings.TALER_CURRENCY, 1)
+ wire_transfer(one, ub, ub_p, subject="a")
+ wire_transfer(one, ub, ub_p, subject="b")
+ wire_transfer(one, ub, ub_p, subject="c")
+ wire_transfer(one, ub, ub_p, subject="d")
+ wire_transfer(one, ub, ub_p, subject="e")
+ wire_transfer(one, ub, ub_p, subject="f")
+ wire_transfer(one, ub, ub_p, subject="g")
+ wire_transfer(one, ub, ub_p, subject="h")
def tearDown(self):
clearDb()
@@ -267,16 +265,34 @@ class HistoryTestCase(TestCase):
**{"HTTP_X_TALER_BANK_USERNAME": "User",
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
self.assertEqual(404, response.status_code)
+class DBAmountSubtraction(TestCase):
+ def setUp(self):
+ a = BankAccount(user=User.objects.create_user(username='U'),
+ amount=Amount(settings.TALER_CURRENCY, 3))
+ a.save()
+
+ def test_subtraction(self):
+ a = BankAccount.objects.get(user=User.objects.get(username='U'))
+ a.amount.subtract(Amount(settings.TALER_CURRENCY, 2))
+ self.assertEqual(0, Amount.cmp(Amount(settings.TALER_CURRENCY, 1),
a.amount))
+
+
+class DBCustomColumnTestCase(TestCase):
+
+ def setUp(TestCase):
+ a = BankAccount(user=User.objects.create_user(username='U'))
+ a.save()
+
+ def test_exists(self):
+ a = BankAccount.objects.get(user=User.objects.get(username='U'))
+ self.assertTrue(isinstance(a.amount, Amount))
# This tests whether a bank account goes debit and then goes >=0 again
class DebitTestCase(TestCase):
def setUp(self):
- u = User.objects.create_user(username='U')
- u0 = User.objects.create_user(username='U0')
- ua = BankAccount(user=u, currency=settings.TALER_CURRENCY)
- u0a = BankAccount(user=u0, currency=settings.TALER_CURRENCY)
-
+ ua = BankAccount(user=User.objects.create_user(username='U'))
+ u0a = BankAccount(user=User.objects.create_user(username='U0'))
ua.save()
u0a.save()
@@ -286,46 +302,40 @@ class DebitTestCase(TestCase):
self.assertEqual(False, ub.debit)
def test_red(self):
- u = User.objects.get(username='U')
- u0 = User.objects.get(username='U0')
-
- ub = BankAccount.objects.get(user=u)
- ub0 = BankAccount.objects.get(user=u0)
+ ub = BankAccount.objects.get(user=User.objects.get(username='U'))
+ ub0 = BankAccount.objects.get(user=User.objects.get(username='U0'))
- wire_transfer(dict(value=10, fraction=0,
currency=settings.TALER_CURRENCY),
+ wire_transfer(Amount(settings.TALER_CURRENCY, 10, 0),
ub0,
ub,
"Go green")
- tmp = amounts.get_zero()
- tmp["value"] = 10
+ tmp = Amount(settings.TALER_CURRENCY, 10)
+ self.assertEqual(0, Amount.cmp(ub.amount, tmp))
+ self.assertEqual(0, Amount.cmp(ub0.amount, tmp))
+ self.assertFalse(ub.debit)
- self.assertEqual(0, amounts.amount_cmp(ub.balance_obj, tmp))
- self.assertEqual(False, ub.debit)
- self.assertEqual(True, ub0.debit)
+ self.assertTrue(ub0.debit)
- wire_transfer(dict(value=11, fraction=0,
currency=settings.TALER_CURRENCY),
+ wire_transfer(Amount(settings.TALER_CURRENCY, 11),
ub,
ub0,
"Go red")
- self.assertEqual(True, ub.debit)
- self.assertEqual(False, ub0.debit)
-
- tmp["value"] = 1
-
- self.assertEqual(0, amounts.amount_cmp(ub0.balance_obj, tmp))
+ tmp.value = 1
+ self.assertTrue(ub.debit)
+ self.assertFalse(ub0.debit)
+ self.assertEqual(0, Amount.cmp(ub.amount, tmp))
+ self.assertEqual(0, Amount.cmp(ub0.amount, tmp))
class ParseAmountTestCase(TestCase):
def test_parse_amount(self):
- ret = amounts.parse_amount("KUDOS:4")
- self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
- ret = amounts.parse_amount("KUDOS:4.00")
- self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', json.dumps(ret))
- ret = amounts.parse_amount("KUDOS:4.3")
- self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency":
"KUDOS"}', json.dumps(ret))
+ ret = Amount.parse("KUDOS:4.0")
+ self.assertJSONEqual('{"value": 4, "fraction": 0, "currency":
"KUDOS"}', ret.dump())
+ ret = Amount.parse("KUDOS:4.3")
+ self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency":
"KUDOS"}', ret.dump())
try:
- amounts.parse_amount("Buggy")
- except amounts.BadFormatAmount:
+ Amount.parse("Buggy")
+ except BadFormatAmount:
return
# make sure the control doesn't get here
self.assertEqual(True, False)
diff --git a/talerbank/app/tests_alt.py b/talerbank/app/tests_alt.py
index cf782d1..4ab9b65 100644
--- a/talerbank/app/tests_alt.py
+++ b/talerbank/app/tests_alt.py
@@ -20,7 +20,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from .models import BankAccount, BankTransaction
from . import urls
-from . import amounts
+from .amount import Amount, BadFormatAmount
from .views import wire_transfer
import json
@@ -35,13 +35,13 @@ class BadDatabaseStringTestCase(TestCase):
class BadMaxDebtOptionTestCase(TestCase):
def test_badmaxdebtoption(self):
try:
- amounts.parse_amount(settings.TALER_MAX_DEBT)
- except amounts.BadFormatAmount:
+ Amount.parse(settings.TALER_MAX_DEBT)
+ except BadFormatAmount:
self.assertTrue(True)
return
try:
- amounts.parse_amount(settings.TALER_MAX_DEBT_BANK)
- except amounts.BadFormatAmount:
+ Amount.parse(settings.TALER_MAX_DEBT_BANK)
+ except BadFormatAmount:
self.assertTrue(True)
return
# Force to have at least one bad amount in config
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index 45328f8..fd6f570 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -37,9 +37,9 @@ import time
import hashlib
import requests
from urllib.parse import urljoin
-from . import amounts
from . import schemas
from .models import BankAccount, BankTransaction
+from .amount import Amount, CurrencyMismatch, BadFormatAmount
logger = logging.getLogger(__name__)
@@ -98,9 +98,9 @@ def profile_page(request):
context = dict(
name=user_account.user.username,
- balance=amounts.stringify(amounts.floatify(user_account.balance_obj)),
+ balance=user_account.amount.stringify(settings.TALER_DIGITS),
sign = "-" if user_account.debit else "",
- currency=user_account.currency,
+ currency=user_account.amount.currency,
precision=settings.TALER_DIGITS,
account_no=user_account.account_no,
history=history,
@@ -149,9 +149,9 @@ def pin_tan_question(request):
currency = request.GET.get("amount_currency", None)
except ValueError:
return HttpResponseBadRequest("invalid parameters: \"amount_currency\"
not given")
- amount = {"value": value,
- "fraction": fraction,
- "currency": currency}
+ if currency != settings.TALER_CURRENCY:
+ return HttpResponse("Such currency (%s) is not accepted" % currency,
status=422)
+ amount = Amount(currency, value, fraction)
user_account = BankAccount.objects.get(user=request.user)
wiredetails = json.loads(request.GET["wire_details"])
if not isinstance(wiredetails, dict) or "test" not in wiredetails:
@@ -160,7 +160,6 @@ def pin_tan_question(request):
"The exchange does not seem to support it.")
try:
schemas.validate_wiredetails(wiredetails)
- schemas.validate_amount(amount)
except ValueError as error:
return HttpResponseBadRequest("invalid parameters (%s)" % error)
# parameters we store in the session are (more or less) validated
@@ -176,7 +175,7 @@ def pin_tan_question(request):
previous_failed = get_session_flag(request, "captcha_failed")
context = dict(
form=Pin(auto_id=False),
- amount=amounts.floatify(amount),
+ amount=amount.stringify(settings.TALER_DIGITS),
previous_failed=previous_failed,
exchange=request.GET["exchange"],
)
@@ -223,7 +222,7 @@ def pin_tan_verify(request):
sender_account_details=sender_wiredetails,
# just something unique
transfer_details=dict(timestamp=int(time.time() * 1000)),
- amount=amount,
+ amount=amount.dump(),
)
user_account = BankAccount.objects.get(user=request.user)
exchange_account =
BankAccount.objects.get(account_no=exchange_account_number)
@@ -233,13 +232,16 @@ def pin_tan_verify(request):
logger.warning("Withdrawal impossible due to debt limit exceeded")
request.session["debt_limit"] = True
return redirect("profile")
- except amounts.BadFormatAmount as e:
- return HttpResponse(e.msg, status=e.status_code)
- except amounts.CurrencyMismatchException as e:
- return HttpResponse(e.msg, status=e.status_code)
except SameAccountException:
logger.error("Odd situation: SameAccountException should NOT occur in
this function")
- return HttpResponse("internal server error", status=500)
+ except BadFormatAmount:
+ logger.error("parsing MAX_DEBT or MAX_BANK_DEBT failed")
+ except CurrencyMismatch:
+ # The only currency mismatch which can occur here is
+ # between the bank and credit/debit accounts', should
+ # never happen.
+ return HttpResponse("Internal server error", status=500)
+
request_url = urljoin(exchange_url, "admin/add/incoming")
res = requests.post(request_url, json=json_body)
@@ -273,22 +275,25 @@ def register(request):
return render(request, "register.html", dict(not_available=True))
with transaction.atomic():
user = User.objects.create_user(username=username, password=password)
- user_account = BankAccount(user=user, currency=settings.TALER_CURRENCY)
+ user_account = BankAccount(user=user)
user_account.save()
bank_internal_account = BankAccount.objects.get(account_no=1)
- amount = dict(value=100, fraction=0, currency=settings.TALER_CURRENCY)
+ print('bank_internal_account currency: ' +
bank_internal_account.amount.currency)
try:
- wire_transfer(amount, bank_internal_account, user_account, "Joining
bonus")
+ wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0),
bank_internal_account, user_account, "Joining bonus")
except DebtLimitExceededException:
logger.info("Debt situation encountered")
request.session["no_initial_bonus"] = True
- except amounts.CurrencyMismatchException as e:
- return HttpResponse(e.msg, status=e.status_code)
- except amounts.BadFormatAmount as e:
- return HttpResponse(e.msg, status=e.status_code)
+ return HttpResponseServerError()
+ except CurrencyMismatch:
+ logger.error("Currency mismatch internal to the bank")
+ return HttpResponseServerError()
+ except BadFormatAmount:
+ logger.error("Could not parse MAX_DEBT|MAX_BANK_DEBT")
+ return HttpResponseServerError()
except SameAccountException:
logger.error("Odd situation: SameAccountException should NOT occur in
this function")
- return HttpResponse("internal server error", status=500)
+ return HttpResponseServerError()
request.session["just_registered"] = True
user = django.contrib.auth.authenticate(username=username,
password=password)
@@ -312,13 +317,13 @@ def extract_history(account):
for item in related_transactions:
if item.credit_account == account:
counterpart = item.debit_account
- sign = 1
+ sign = ""
else:
counterpart = item.credit_account
- sign = -1
+ sign = "-"
entry = dict(
- float_amount=amounts.stringify(amounts.floatify(item.amount_obj) *
sign),
- float_currency=item.currency,
+ sign = sign,
+ amount = item.amount.stringify(settings.TALER_DIGITS),
counterpart=counterpart.account_no,
counterpart_username=counterpart.user.username,
subject=item.subject,
@@ -421,7 +426,7 @@ def history(request):
counterpart = entry.debit_account.account_no
sign_ = "+"
history.append(dict(counterpart=counterpart,
- amount=entry.amount_obj,
+ amount=entry.amount.dump(),
sign=sign_,
wt_subject=entry.subject,
row_id=entry.id,
@@ -484,29 +489,37 @@ def add_incoming(request):
except BankAccount.DoesNotExist:
return HttpResponse(status=404)
try:
- transaction = wire_transfer(data["amount"],
+ schemas.validate_amount(data["amount"])
+ if settings.TALER_CURRENCY != data["amount"]["currency"]:
+ logger.error("Currency differs from bank's")
+ return JsonResponse(dict(error="Currency differs from bank's"),
status=406)
+ transaction = wire_transfer(Amount(**data["amount"]),
user_account.bankaccount,
credit_account,
subject)
return JsonResponse(dict(serial_id=transaction.id,
timestamp="/Date(%s)/" % int(transaction.date.timestamp())))
- except amounts.BadFormatAmount as e:
- return JsonResponse(dict(error=e.msg), status=e.status_code)
+ except ValueError as e:
+ return JsonResponse(dict(error=e), status=400)
+
+ except BadFormatAmount:
+ logger("Bad MAX_DEBT|MAX_BANK_DEBT format")
+ except CurrencyMismatch:
+ logger.error("Internal currency inconsistency")
+ return JsonResponse(dict(error="Internal server error"), status=500)
except SameAccountException:
return JsonResponse(dict(error="debit and credit account are the
same"), status=422)
except DebtLimitExceededException:
logger.info("Prevenetd transfer, debit account would go beyond debt
threshold")
return JsonResponse(dict(error="debit count has reached its debt
limit", status=403 ),
status=403)
- except amounts.CurrencyMismatchException as e:
- return JsonResponse(dict(error=e.msg), status=e.status_code)
@login_required
@require_POST
def withdraw_nojs(request):
try:
- amount = amounts.parse_amount(request.POST.get("kudos_amount", ""))
- except amounts.BadFormatAmount:
+ amount = Amount.parse(request.POST.get("kudos_amount", ""))
+ except BadFormatAmount:
logger.error("Amount did not pass parsing")
return HttpResponseBadRequest()
@@ -516,7 +529,7 @@ def withdraw_nojs(request):
response["X-Taler-Operation"] = "create-reserve"
response["X-Taler-Callback-Url"] = reverse("pin-question")
response["X-Taler-Wt-Types"] = '["test"]'
- response["X-Taler-Amount"] = json.dumps(amount)
+ response["X-Taler-Amount"] = amount.stringify(settings.TALER_CURRENCY)
response["X-Taler-Sender-Wire"] = json.dumps(dict(
type="test",
bank_uri=request.build_absolute_uri(reverse("index")),
@@ -527,81 +540,48 @@ def withdraw_nojs(request):
return response
-def wire_transfer(amount,
- debit_account,
- credit_account,
- subject):
+def wire_transfer(amount, debit_account, credit_account, subject):
if debit_account.pk == credit_account.pk:
logger.error("Debit and credit account are the same!")
raise SameAccountException()
- transaction_item = BankTransaction(amount_value=amount["value"],
- amount_fraction=amount["fraction"],
- currency=amount["currency"],
+ transaction_item = BankTransaction(amount=amount,
credit_account=credit_account,
debit_account=debit_account,
subject=subject)
-
- try:
- if debit_account.debit:
- debit_account.balance_obj =
amounts.amount_add(debit_account.balance_obj,
- amount)
-
- elif -1 == amounts.amount_cmp(debit_account.balance_obj, amount):
- debit_account.debit = True
- debit_account.balance_obj = amounts.amount_sub(amount,
-
debit_account.balance_obj)
- else:
- debit_account.balance_obj =
amounts.amount_sub(debit_account.balance_obj,
- amount)
-
- if False == credit_account.debit:
- credit_account.balance_obj =
amounts.amount_add(credit_account.balance_obj,
- amount)
-
- elif 1 == amounts.amount_cmp(amount, credit_account.balance_obj):
- credit_account.debit = False
- credit_account.balance_obj = amounts.amount_sub(amount,
-
credit_account.balance_obj)
- else:
- credit_account.balance_obj =
amounts.amount_sub(credit_account.balance_obj,
- amount)
- except amounts.CurrencyMismatchException:
- msg = "The amount to be transferred (%s) doesn't match the bank's
currency (%s)" % (amount["currency"], settings.TALER_CURRENCY)
- status_code = 406
- if settings.TALER_CURRENCY != credit_account.balance_obj["currency"]:
- logger.error("Internal inconsistency: credit account's currency
(%s) differs from bank's (%s)" % (credit_account.balance_obj["currency"],
settings.TALER_CURRENCY))
- msg = "Internal server error"
- status_code = 500
- elif settings.TALER_CURRENCY != debit_account.balance_obj["currency"]:
- logger.error("Internal inconsistency: debit account's currency
(%s) differs from bank's (%s)" % (debit_account.balance_obj["currency"],
settings.TALER_CURRENCY))
- msg = "Internal server error"
- status_code = 500
- logger.error(msg)
- raise amounts.CurrencyMismatchException(msg=msg,
status_code=status_code)
+ 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 1 == Amount.cmp(amount, credit_account.amount):
+ 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.
- try:
- threshold = amounts.parse_amount(settings.TALER_MAX_DEBT)
-
- if debit_account.user.username == "Bank":
- threshold = amounts.parse_amount(settings.TALER_MAX_DEBT_BANK)
- except amounts.BadFormatAmount:
- logger.error("MAX_DEBT|MAX_DEBT_BANK had the wrong format")
- raise amounts.BadFormatAmount(msg="internal server error",
status_code=500)
-
- try:
- if 1 == amounts.amount_cmp(debit_account.balance_obj, threshold) \
- and 0 != amounts.amount_cmp(amounts.get_zero(), threshold) \
- and debit_account.debit:
- logger.info("Negative balance '%s' not allowed." %
json.dumps(debit_account.balance_obj))
- logger.info("%s's threshold is: '%s'." %
(debit_account.user.username, json.dumps(threshold)))
- raise DebtLimitExceededException()
- except amounts.CurrencyMismatchException:
- logger.error("(Internal) currency mismatch between debt threshold and
debit account")
- raise amounts.CurrencyMismatchException(msg="internal server error",
status_code=500)
+ threshold = Amount.parse(settings.TALER_MAX_DEBT)
+ if debit_account.user.username == "Bank":
+ threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK)
+ if 1 == Amount.cmp(debit_account.amount, threshold) \
+ and 0 != Amount.cmp(Amount(settings.TALER_CURRENCY), threshold) \
+ and debit_account.debit:
+ logger.info("Negative balance '%s' not allowed." %
json.dumps(debit_account.amount.dump()))
+ logger.info("%s's threshold is: '%s'." % (debit_account.user.username,
json.dumps(threshold.dump())))
+ raise DebtLimitExceededException()
with transaction.atomic():
debit_account.save()
diff --git a/talerbank/settings.py b/talerbank/settings.py
index ccaee76..3dad905 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -187,8 +187,8 @@ except ConfigurationError as e:
logger.error(e)
sys.exit(3)
-TALER_MAX_DEBT = tc.value_string("bank", "MAX_DEBT", default="%s:50" %
TALER_CURRENCY)
-TALER_MAX_DEBT_BANK = tc.value_string("bank", "MAX_DEBT_BANK", default="%s:0"
% TALER_CURRENCY)
+TALER_MAX_DEBT = tc.value_string("bank", "MAX_DEBT", default="%s:50.0" %
TALER_CURRENCY)
+TALER_MAX_DEBT_BANK = tc.value_string("bank", "MAX_DEBT_BANK",
default="%s:0.0" % TALER_CURRENCY)
TALER_DIGITS = tc.value_int("bank", "NDIGITS", default=2)
TALER_PREDEFINED_ACCOUNTS = ['Tor', 'GNUnet', 'Taler', 'FSF', 'Tutorial']
--
To stop receiving notification emails like this one, please contact
address@hidden