Browse Source

Added transaction support

pull/7/head
Kenneth Bruen 3 years ago
parent
commit
5313b4cecd
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 9
      server/foxbank_server/apis/accounts.py
  2. 4
      server/foxbank_server/apis/login.py
  3. 83
      server/foxbank_server/apis/transactions.py
  4. 61
      server/foxbank_server/db_utils.py
  5. 20
      server/foxbank_server/models.py
  6. 14
      server/foxbank_server/returns.py
  7. 8
      server/init.sql

9
server/foxbank_server/apis/accounts.py

@ -49,7 +49,7 @@ def get_account_id(account_id: int):
return returns.abort(returns.NOT_FOUND) return returns.abort(returns.NOT_FOUND)
if decorators.user_id != db_utils.whose_account(account): if decorators.user_id != db_utils.whose_account(account):
return returns.abort(returns.UNAUTHORIZED) return returns.abort(returns.UNAUTHORIZED)
account = account.to_json() # account = account.to_json()
return returns.success(account=account) return returns.success(account=account)
@ -65,7 +65,7 @@ def get_account_iban(iban: str):
return returns.abort(returns.NOT_FOUND) return returns.abort(returns.NOT_FOUND)
if decorators.user_id != db_utils.whose_account(account): if decorators.user_id != db_utils.whose_account(account):
return returns.abort(returns.UNAUTHORIZED) return returns.abort(returns.UNAUTHORIZED)
account = account.to_json() # account = account.to_json()
return returns.success(account=account) return returns.success(account=account)
@ -84,7 +84,7 @@ class AccountsList(MethodView):
@bp.doc(security=[{'Token': []}]) @bp.doc(security=[{'Token': []}])
@bp.arguments(CreateAccountParams, as_kwargs=True) @bp.arguments(CreateAccountParams, as_kwargs=True)
@bp.response(200, CreateAccountResponseSchema) @bp.response(200, CreateAccountResponseSchema)
@bp.response(HTTPStatus.UNPROCESSABLE_ENTITY, description='Invalid currency or account type') @bp.response(422, returns.ErrorSchema, description='Invalid currency or account type')
def post(self, currency: str, account_type: str, custom_name: str): def post(self, currency: str, account_type: str, custom_name: str):
"""Create account""" """Create account"""
if currency not in VALID_CURRENCIES: if currency not in VALID_CURRENCIES:
@ -94,7 +94,8 @@ class AccountsList(MethodView):
account = Account(-1, '', currency, account_type, custom_name or '') account = Account(-1, '', currency, account_type, custom_name or '')
db_utils.insert_account(decorators.user_id, account) db_utils.insert_account(decorators.user_id, account)
return returns.success(account=account.to_json()) # return returns.success(account=account.to_json())
return returns.success(account=account)
class AccountsResponseSchema(returns.SuccessSchema): class AccountsResponseSchema(returns.SuccessSchema):
accounts = fields.List(fields.Nested(Account.AccountSchema)) accounts = fields.List(fields.Nested(Account.AccountSchema))

4
server/foxbank_server/apis/login.py

@ -67,7 +67,7 @@ class WhoAmI(MethodView):
def get(self): def get(self):
"""Get information about currently logged in user""" """Get information about currently logged in user"""
user: User | None = get_user(user_id=decorators.user_id) user: User | None = get_user(user_id=decorators.user_id)
if user is not None: # if user is not None:
user = user.to_json() # user = user.to_json()
return returns.success(user=user) return returns.success(user=user)

83
server/foxbank_server/apis/transactions.py

@ -1,11 +1,15 @@
from datetime import date, datetime
from flask.views import MethodView from flask.views import MethodView
from flask_smorest import Blueprint from flask_smorest import Blueprint
from marshmallow import Schema, fields from marshmallow import Schema, fields
import re
from ..decorators import ensure_logged_in from ..decorators import ensure_logged_in
from ..models import Transaction from ..db_utils import get_transactions, get_account, get_accounts, insert_transaction, whose_account
from ..db_utils import get_transactions from ..models import Account, Transaction
from .. import returns from ..utils.iban import check_iban
from .. import decorators, returns
bp = Blueprint('transactions', __name__, description='Bank transfers and other transactions') bp = Blueprint('transactions', __name__, description='Bank transfers and other transactions')
@ -18,29 +22,90 @@ class TransactionsList(MethodView):
transactions = fields.List(fields.Nested(Transaction.TransactionSchema)) transactions = fields.List(fields.Nested(Transaction.TransactionSchema))
@ensure_logged_in @ensure_logged_in
@bp.response(401, returns.ErrorSchema, description='Login failure') @bp.response(401, returns.ErrorSchema, description='Login failure or not allowed')
@bp.doc(security=[{'Token': []}]) @bp.doc(security=[{'Token': []}])
@bp.arguments(TransactionsParams, as_kwargs=True, location='query') @bp.arguments(TransactionsParams, as_kwargs=True, location='query')
@bp.response(200, TransactionsGetResponse) @bp.response(200, TransactionsGetResponse)
def get(self, account_id: int): def get(self, account_id: int):
"""Get transactions for a certain account""" """Get transactions for a certain account"""
if whose_account(account_id) != decorators.user_id:
return returns.abort(returns.UNAUTHORIZED)
# return returns.success(
# transactions=[t.to_json() for t in get_transactions(account_id)]
# )
return returns.success( return returns.success(
transactions=[t.to_json() for t in get_transactions(account_id)] transactions=get_transactions(account_id)
) )
class TransactionsCreateParams(Schema): class TransactionsCreateParams(Schema):
account_id = fields.Int(min=1) account_id = fields.Int(min=1)
destination_iban = fields.Str() destination_iban = fields.Str()
amount = fields.Int(min=1) amount = fields.Int(min=1)
description = fields.Str(default='')
class TransactionsCreateResponse(returns.SuccessSchema): class TransactionsCreateResponse(returns.SuccessSchema):
transaction = fields.Nested(Transaction.TransactionSchema) transaction = fields.Nested(Transaction.TransactionSchema)
@ensure_logged_in @ensure_logged_in
@bp.response(401, returns.ErrorSchema, description='Login failure') @bp.response(401, returns.ErrorSchema, description='Login failure or not allowed')
@bp.response(404, returns.ErrorSchema, description='Destination account not found')
@bp.response(422, returns.ErrorSchema, description='Invalid account')
@bp.doc(security=[{'Token': []}]) @bp.doc(security=[{'Token': []}])
@bp.arguments(TransactionsCreateParams) @bp.arguments(TransactionsCreateParams, as_kwargs=True)
@bp.response(200, TransactionsCreateResponse) @bp.response(200, TransactionsCreateResponse)
def post(self, account_id: int, destination_iban: str, amount: int): def post(self, account_id: int, destination_iban: str, amount: int, description: str = ''):
"""Create a send_transfer transaction""" """Create a send_transfer transaction"""
raise NotImplementedError() if whose_account(account_id) != decorators.user_id:
return returns.abort(returns.UNAUTHORIZED)
account: Account = get_account(account_id=account_id)
if account is None:
return returns.abort(returns.invalid_argument('account_id'))
amount = -1 * abs(amount)
if account.balance + amount < 0:
return returns.abort(returns.NO_BALANCE)
# Check if IBAN is valid
destination_iban = re.sub(r'\s', '', destination_iban)
if not check_iban(destination_iban):
return returns.abort(returns.INVALID_IBAN)
date = datetime.now()
# Check if transaction is to another FoxBank account
reverse_transaction = None
if destination_iban[4:8] == 'FOXB':
for acc in get_accounts():
if destination_iban == acc.iban:
reverse_transaction = Transaction.new_transaction(
date_time=date,
transaction_type='receive_transfer',
status='processed',
other_party={'iban': account.iban,},
extra={'currency': account.currency, 'amount': -amount,},
)
insert_transaction(acc.id, reverse_transaction)
break
else:
return returns.abort(returns.NOT_FOUND)
transaction = Transaction.new_transaction(
date_time=date,
transaction_type='send_transfer',
status=('processed' if reverse_transaction is not None else 'pending'),
other_party={'iban': destination_iban,},
extra={
'currency': account.currency,
'amount': amount,
'description': description,
},
)
insert_transaction(account_id, transaction)
return returns.success(transaction=transaction)

61
server/foxbank_server/db_utils.py

@ -1,4 +1,5 @@
from functools import wraps from functools import wraps
import json
import sys import sys
from types import ModuleType from types import ModuleType
@ -85,7 +86,19 @@ class Module(ModuleType):
''', (user_id,)) ''', (user_id,))
else: else:
cur.execute('select id, iban, currency, account_type, custom_name from accounts') cur.execute('select id, iban, currency, account_type, custom_name from accounts')
return [models.Account.from_query(q) for q in cur.fetchall()] accounts = [models.Account.from_query(q) for q in cur.fetchall()]
for account in accounts:
cur.execute(
'select balance from V_account_balance where account_id = ?',
(account.id,),
)
result = cur.fetchone()
if result is not None:
account.balance = result['balance']
return accounts
@get_db @get_db
@ -106,7 +119,18 @@ class Module(ModuleType):
result = cur.fetchone() result = cur.fetchone()
if result is None: if result is None:
return None return None
return models.Account.from_query(result) account = models.Account.from_query(result)
cur.execute(
'select balance from V_account_balance where account_id = ?',
(account.id,),
)
result = cur.fetchone()
if result is not None:
account.balance = result['balance']
return account
@get_db @get_db
@ -180,4 +204,37 @@ class Module(ModuleType):
return transactions return transactions
@get_db
def insert_transaction(self, account_id: int, transaction: models.Transaction):
cur = self.db.cursor()
cur.execute(
'insert into transactions(datetime, other_party, status, type, extra) values (?, ?, ?, ?, ?)',
(
transaction.date_time.isoformat(),
json.dumps(transaction.other_party),
transaction.status,
transaction.transaction_type,
json.dumps(transaction.extra),
),
)
cur.execute(
'select id from transactions where datetime = ? and other_party = ? and status = ? and type = ? and extra = ?',
(
transaction.date_time.isoformat(),
json.dumps(transaction.other_party),
transaction.status,
transaction.transaction_type,
json.dumps(transaction.extra),
),
)
transaction.id = cur.fetchone()['id']
cur.execute(
'insert into accounts_transactions(account_id, transaction_id) VALUES (?, ?)',
(account_id, transaction.id),
)
self.db.commit()
sys.modules[__name__] = Module(__name__) sys.modules[__name__] = Module(__name__)

20
server/foxbank_server/models.py

@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from marshmallow import Schema, fields from marshmallow import Schema, fields
from datetime import datetime from datetime import datetime
@ -51,6 +51,7 @@ class Account:
currency: str currency: str
account_type: str account_type: str
custom_name: str custom_name: str
balance: int = field(default=0)
class AccountSchema(Schema): class AccountSchema(Schema):
id = fields.Int(required=False) id = fields.Int(required=False)
@ -58,6 +59,7 @@ class Account:
currency = fields.Str() currency = fields.Str()
account_type = fields.Str(data_key='accountType') account_type = fields.Str(data_key='accountType')
custom_name = fields.Str(data_key='customName') custom_name = fields.Str(data_key='customName')
balance = fields.Int()
@staticmethod @staticmethod
def new_account(currency: str, account_type: str, custom_name: str = '') -> 'Account': def new_account(currency: str, account_type: str, custom_name: str = '') -> 'Account':
@ -75,6 +77,7 @@ class Account:
'currency': self.currency, 'currency': self.currency,
'accountType': self.account_type, 'accountType': self.account_type,
'customName': self.custom_name, 'customName': self.custom_name,
'balance': self.balance,
} }
if include_id: if include_id:
result['id'] = self.id result['id'] = self.id
@ -97,9 +100,10 @@ class Transaction:
class TransactionSchema(Schema): class TransactionSchema(Schema):
id = fields.Int(required=False) id = fields.Int(required=False)
date_time = fields.DateTime(data_key='datetime') date_time = fields.DateTime(data_key='datetime')
other_party = fields.Str(data_key='otherParty') other_party = fields.Dict(keys=fields.Str(), values=fields.Raw(), data_key='otherParty')
status = fields.Str()
transaction_type = fields.Str(data_key='transactionType') transaction_type = fields.Str(data_key='transactionType')
extra = fields.Str() extra = fields.Dict(keys=fields.Str(), values=fields.Raw())
@staticmethod @staticmethod
def new_transaction(date_time: datetime, other_party: str, status: str, transaction_type: str, extra: str = '') -> 'Account': def new_transaction(date_time: datetime, other_party: str, status: str, transaction_type: str, extra: str = '') -> 'Account':
@ -126,4 +130,14 @@ class Transaction:
@classmethod @classmethod
def from_query(cls, query_result): def from_query(cls, query_result):
import json
query_result = list(query_result)
if type(query_result[1]) is str:
query_result[1] = datetime.fromisoformat(query_result[1])
if type(query_result[2]) is str:
query_result[2] = json.loads(query_result[2])
if type(query_result[5]) is str:
query_result[5] = json.loads(query_result[5])
return cls(*query_result) return cls(*query_result)

14
server/foxbank_server/returns.py

@ -61,6 +61,20 @@ UNAUTHORIZED = _make_error(
"You are logged in but the resource you're trying to access isn't available to you", "You are logged in but the resource you're trying to access isn't available to you",
) )
# Transactions
NO_BALANCE = _make_error(
_HTTPStatus.BAD_REQUEST,
'transaction/no_balance',
'Not enough balance to make the transaction',
)
INVALID_IBAN = _make_error(
_HTTPStatus.BAD_REQUEST,
'transaction/invalid_iban',
'Recipient IBAN is invalid',
)
# Success # Success

8
server/init.sql

@ -59,3 +59,11 @@ create table users_notifications (
foreign key (user_id) references users (id), foreign key (user_id) references users (id),
foreign key (notification_id) references notifications (id) foreign key (notification_id) references notifications (id)
); );
create view V_account_balance as
select
accounts_transactions.account_id as "account_id",
sum(json_extract(transactions.extra, '$.amount')) as "balance"
from transactions
inner join accounts_transactions on accounts_transactions.transaction_id = transactions.id
group by accounts_transactions.account_id;

Loading…
Cancel
Save