mirror of
https://github.com/dancojocaru2000/foxbank.git
synced 2025-02-23 08:09:35 +02:00
Added transaction support
This commit is contained in:
parent
5da66e520b
commit
5313b4cecd
7 changed files with 180 additions and 21 deletions
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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…
Add table
Reference in a new issue