## # This file is part of TALER # (C) 2014, 2015, 2016 Taler Systems SA # # 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 # # # @author Marcello Stanisci # @author Florian Dold import uuid from typing import Any, Tuple from django.contrib.auth.models import User from django.db import models from django.conf import settings from django.core.exceptions import \ ValidationError, \ ObjectDoesNotExist from taler.util.amount import Amount, BadFormatAmount, NumberTooBig, CurrencyMismatch class InvalidAmount(Amount): def __init__(self, currency): super(InvalidAmount, self ).__init__(currency, value=float('nan'), fraction=float('nan')) def stringify(self, ndigits, pretty): return "Invalid Amount, please report" def dump(self): return "Invalid Amount, please report" ## # Helper function that instantiates a zero-valued @a Amount # object. def get_zero_amount() -> Amount: return Amount(settings.TALER_CURRENCY) ## # Custom implementation of the @a Amount class as a database type. class AmountField(models.Field): description = 'Amount object in Taler style' ## # Return the database type of the serialized amount. # # @param self the object itself. # @param connection the database connection. # @return type of the serialized amount: varchar. def db_type(self, connection: Any) -> str: return "varchar" ## # Stringifies the Amount object to feed the DB connector. # # @param self the object itself. # @para value the @a Amount object to be serialized. def get_prep_value(self, value: Amount) -> str: if not value: return "%s:0.0" % settings.TALER_CURRENCY if settings.TALER_CURRENCY != value.currency: raise CurrencyMismatch(settings.TALER_CURRENCY, value.currency) return value.stringify(settings.TALER_DIGITS) ## # Parse the stringified Amount back to Python. # # @param value serialized amount coming from the database. # (It is just a string in the usual CURRENCY:X.Y form) # @param args currently unused. # @return the @a Amount object. @staticmethod def from_db_value(value: str, *args) -> Amount: del args # pacify PEP checkers if value is None: return Amount.parse(settings.TALER_CURRENCY) try: return Amount.parse(value) except NumberTooBig: # Keep the currency right to avoid causing # exceptions if some operation is attempted # against this invalid amount. NOTE that the # value is defined as NaN, so no actual/useful # amount will ever be generated using this one. # And more, the NaN value will make it easier # to scan the database to find these faulty # amounts. # We also decide to not raise exception here # because they would propagate in too many places # in the code, and it would be too verbose to # just try-cactch any possible exception situation. return InvalidAmount(settings.TALER_CURRENCY) ## # Parse the stringified Amount back to Python. FIXME: # why this serializer consider _more_ cases respect to the # one above ('from_db_value')? # # @param value serialized amount coming from the database. # (It is just a string in the usual CURRENCY:X.Y form) # @param args currently unused. # @return the @a Amount object. def to_python(self, value: Any) -> Amount: if isinstance(value, Amount): return value try: if value is None: return Amount.parse(settings.TALER_CURRENCY) return Amount.parse(value) except BadFormatAmount: raise ValidationError( "Invalid input for an amount string: %s" % value) class BankAccountDoesNotExist(Exception): def __init__(self): self.hint = "Bank account not found" self.http_status_code = 404 self.taler_error_code = 5110 self.minor_error_code = 0 class BankTransactionDoesNotExist(Exception): def __init__(self): self.hint = "Bank transaction not found" self.http_status_code = 404 self.taler_error_code = 5111 self.minor_error_code = 0 class CustomManager(models.Manager): def __init__(self): super(CustomManager, self).__init__() def get_queryset(self): return models.QuerySet(self.model, using=self._db) def get(self, *args, **kwargs): try: return super(CustomManager, self).get(*args, **kwargs) except BankAccount.DoesNotExist: raise BankAccountDoesNotExist() except BankTransaction.DoesNotExist: raise BankTransactionDoesNotExist() ## # The class representing a bank account. class BankAccount(models.Model): is_public = models.BooleanField(default=False) debit = models.BooleanField(default=False) account_no = models.AutoField(primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE) amount = AmountField(default=get_zero_amount) objects = CustomManager() ## # The class representing a bank transaction. class BankTransaction(models.Model): amount = AmountField(default=False) debit_account = models.ForeignKey( BankAccount, on_delete=models.CASCADE, db_index=True, related_name="debit_account" ) credit_account = models.ForeignKey( BankAccount, on_delete=models.CASCADE, db_index=True, related_name="credit_account" ) subject = models.CharField(default="(no subject given)", max_length=200) date = models.DateTimeField(auto_now=True, db_index=True) cancelled = models.BooleanField(default=False) objects = CustomManager() class TalerWithdrawOperation(models.Model): withdraw_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) amount = AmountField(default=False) withdraw_account = models.ForeignKey( BankAccount, on_delete=models.CASCADE, db_index=True, related_name="withdraw_account" ) selection_done = models.BooleanField(default=False) withdraw_done = models.BooleanField(default=False) selected_exchange_account = models.ForeignKey( BankAccount, null=True, on_delete=models.CASCADE, related_name="selected_exchange_account" ) selected_reserve_pub = models.TextField(null=True)