mirror of
https://github.com/dancojocaru2000/foxbank.git
synced 2025-02-22 23:39:36 +02:00
commit
5e19ab457b
11 changed files with 529 additions and 26 deletions
3
server/.vscode/launch.json
vendored
3
server/.vscode/launch.json
vendored
|
@ -11,7 +11,8 @@
|
|||
"module": "flask",
|
||||
"env": {
|
||||
"FLASK_APP": "server.py",
|
||||
"FLASK_ENV": "development"
|
||||
"FLASK_ENV": "development",
|
||||
"FLASK_RUN_PORT": "5001"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
|
|
|
@ -3,6 +3,8 @@ from flask_smorest import Api
|
|||
|
||||
from .accounts import bp as acc_bp
|
||||
from .login import bp as login_bp
|
||||
from .transactions import bp as transactions_bp
|
||||
from .notifications import bp as notifications_bp
|
||||
|
||||
class ApiWithErr(Api):
|
||||
def handle_http_exception(self, error):
|
||||
|
@ -27,3 +29,5 @@ def init_apis(app: Flask):
|
|||
})
|
||||
api.register_blueprint(login_bp, url_prefix='/login')
|
||||
api.register_blueprint(acc_bp, url_prefix='/accounts')
|
||||
api.register_blueprint(transactions_bp, url_prefix='/transactions')
|
||||
api.register_blueprint(notifications_bp, url_prefix='/notifications')
|
||||
|
|
|
@ -2,8 +2,12 @@ from http import HTTPStatus
|
|||
from flask.views import MethodView
|
||||
from flask_smorest import Blueprint, abort
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
import re
|
||||
|
||||
from ..decorators import ensure_logged_in
|
||||
from ..models import Account
|
||||
from ..utils.iban import IBAN_BANKS, check_iban
|
||||
from .. import decorators
|
||||
from .. import db_utils
|
||||
from .. import returns
|
||||
|
@ -13,14 +17,20 @@ bp = Blueprint('accounts', __name__, description='Bank Accounts operations')
|
|||
VALID_CURRENCIES = ['RON', 'EUR', 'USD']
|
||||
ACCOUNT_TYPES = ['Checking', 'Savings']
|
||||
|
||||
class MetaCurrenciesSchema(Schema):
|
||||
status = fields.Constant('success')
|
||||
class MetaCurrenciesSchema(returns.SuccessSchema):
|
||||
currencies = fields.List(fields.Str())
|
||||
|
||||
class MetaAccountTypesSchema(Schema):
|
||||
status = fields.Constant('success')
|
||||
class MetaAccountTypesSchema(returns.SuccessSchema):
|
||||
account_types = fields.List(fields.Str(), data_key='accountTypes')
|
||||
|
||||
class MetaValidateIbanParams(Schema):
|
||||
iban = fields.Str(example='RO15RZBR0000060021338765')
|
||||
|
||||
class MetaValidateIbanSchema(returns.SuccessSchema):
|
||||
valid = fields.Bool()
|
||||
formatted_iban = fields.Str(data_key='formattedIban', optional=True)
|
||||
bank_name = fields.Str(data_key='bankName', optional=True, description='Known bank for IBAN')
|
||||
|
||||
@bp.get('/meta/currencies')
|
||||
@bp.response(200, MetaCurrenciesSchema)
|
||||
def get_valid_currencies():
|
||||
|
@ -35,13 +45,32 @@ def get_valid_account_types():
|
|||
return returns.success(account_types=ACCOUNT_TYPES)
|
||||
|
||||
|
||||
@bp.get('/meta/validate_iban')
|
||||
@bp.arguments(MetaValidateIbanParams, location='query', as_kwargs=True)
|
||||
@bp.response(200, MetaValidateIbanSchema)
|
||||
def get_validate_iban(iban: str):
|
||||
"""Validate IBAN"""
|
||||
iban = re.sub(r'\s', '', iban)
|
||||
valid = len(iban) > 8 and re.match(r'^[A-Z]{2}[0-9]{2}', iban) is not None and check_iban(iban)
|
||||
bank_name = None
|
||||
if iban[0:2] in IBAN_BANKS:
|
||||
if iban[4:8] in IBAN_BANKS[iban[0:2]]:
|
||||
bank_name = IBAN_BANKS[iban[0:2]][iban[4:8]]
|
||||
|
||||
return returns.success(
|
||||
valid=valid,
|
||||
formatted_iban=re.sub(r'(.{4})', r'\1 ', iban).strip() if valid else None,
|
||||
bank_name=bank_name if valid else None,
|
||||
)
|
||||
|
||||
|
||||
class AccountResponseSchema(returns.SuccessSchema):
|
||||
account = fields.Nested(Account.Schema)
|
||||
account = fields.Nested(Account.AccountSchema)
|
||||
|
||||
|
||||
@bp.get('/<int:account_id>')
|
||||
@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.response(200, AccountResponseSchema)
|
||||
def get_account_id(account_id: int):
|
||||
|
@ -51,7 +80,7 @@ def get_account_id(account_id: int):
|
|||
return returns.abort(returns.NOT_FOUND)
|
||||
if decorators.user_id != db_utils.whose_account(account):
|
||||
return returns.abort(returns.UNAUTHORIZED)
|
||||
account = account.to_json()
|
||||
# account = account.to_json()
|
||||
return returns.success(account=account)
|
||||
|
||||
|
||||
|
@ -67,26 +96,26 @@ def get_account_iban(iban: str):
|
|||
return returns.abort(returns.NOT_FOUND)
|
||||
if decorators.user_id != db_utils.whose_account(account):
|
||||
return returns.abort(returns.UNAUTHORIZED)
|
||||
account = account.to_json()
|
||||
# account = account.to_json()
|
||||
return returns.success(account=account)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
class AccountsList(MethodView):
|
||||
class CreateAccountParams(Schema):
|
||||
currency = fields.String()
|
||||
account_type = fields.String(data_key='accountType')
|
||||
custom_name = fields.String(data_key='customName')
|
||||
currency = fields.String(example='RON')
|
||||
account_type = fields.String(data_key='accountType', example='Checking')
|
||||
custom_name = fields.String(data_key='customName', example='Daily Spending')
|
||||
|
||||
class CreateAccountResponseSchema(returns.SuccessSchema):
|
||||
account = fields.Nested(Account.Schema)
|
||||
account = fields.Nested(Account.AccountSchema)
|
||||
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure')
|
||||
@bp.doc(security=[{'Token': []}])
|
||||
@bp.arguments(CreateAccountParams, as_kwargs=True)
|
||||
@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):
|
||||
"""Create account"""
|
||||
if currency not in VALID_CURRENCIES:
|
||||
|
@ -96,10 +125,11 @@ class AccountsList(MethodView):
|
|||
|
||||
account = Account(-1, '', currency, account_type, custom_name or '')
|
||||
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):
|
||||
accounts = fields.List(fields.Nested(Account.Schema))
|
||||
accounts = fields.List(fields.Nested(Account.AccountSchema))
|
||||
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure')
|
||||
|
|
|
@ -67,7 +67,7 @@ class WhoAmI(MethodView):
|
|||
def get(self):
|
||||
"""Get information about currently logged in user"""
|
||||
user: User | None = get_user(user_id=decorators.user_id)
|
||||
if user is not None:
|
||||
user = user.to_json()
|
||||
# if user is not None:
|
||||
# user = user.to_json()
|
||||
|
||||
return returns.success(user=user)
|
||||
|
|
60
server/foxbank_server/apis/notifications.py
Normal file
60
server/foxbank_server/apis/notifications.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from datetime import datetime
|
||||
from flask.views import MethodView
|
||||
from flask_smorest import Blueprint
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
from ..db_utils import get_notifications, insert_notification, mark_notification_as_read, whose_notification
|
||||
from ..decorators import ensure_logged_in
|
||||
from ..models import Notification
|
||||
from .. import decorators, returns
|
||||
|
||||
bp = Blueprint('notifications', __name__, description='Notifications operations')
|
||||
|
||||
@bp.post('/<int:notification_id>/mark_read')
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure or not allowed')
|
||||
@bp.doc(security=[{'Token': []}])
|
||||
@bp.response(201, description='Successfully marked as read')
|
||||
def mark_as_read(notification_id: int):
|
||||
"""Mark notification as read"""
|
||||
if decorators.user_id != whose_notification(notification_id):
|
||||
return returns.abort(returns.UNAUTHORIZED)
|
||||
mark_notification_as_read(notification_id)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
class NotificationsList(MethodView):
|
||||
class NotificationsListPostParams(Schema):
|
||||
body = fields.Str(description='Text of the notification')
|
||||
read = fields.Bool(default=False, description='Whether the notification was read or not')
|
||||
|
||||
class NotificationsListPostSchema(returns.SuccessSchema):
|
||||
notification = fields.Nested(Notification.NotificationSchema)
|
||||
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure')
|
||||
@bp.doc(security=[{'Token': []}])
|
||||
@bp.arguments(NotificationsListPostParams, as_kwargs=True)
|
||||
@bp.response(200, NotificationsListPostSchema)
|
||||
def post(self, body: str, read: bool = False):
|
||||
"""Post a notification to the currently logged in user
|
||||
|
||||
The usefulness of this endpoint is questionable besides debugging since it's a notification to self
|
||||
"""
|
||||
now = datetime.now()
|
||||
notification = Notification.new_notification(body, now, read)
|
||||
insert_notification(decorators.user_id, notification)
|
||||
return returns.success(notification=notification)
|
||||
|
||||
class NotificationsListGetSchema(returns.SuccessSchema):
|
||||
notifications = fields.List(fields.Nested(Notification.NotificationSchema))
|
||||
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure')
|
||||
@bp.doc(security=[{'Token': []}])
|
||||
@bp.response(200, NotificationsListGetSchema)
|
||||
def get(self):
|
||||
"""Get all notifications for current user"""
|
||||
notifications = get_notifications(decorators.user_id)
|
||||
|
||||
return returns.success(notifications=notifications)
|
122
server/foxbank_server/apis/transactions.py
Normal file
122
server/foxbank_server/apis/transactions.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from datetime import date, datetime
|
||||
from flask.views import MethodView
|
||||
from flask_smorest import Blueprint
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
import re
|
||||
|
||||
from ..decorators import ensure_logged_in
|
||||
from ..db_utils import get_transactions, get_account, get_accounts, insert_transaction, whose_account, insert_notification
|
||||
from ..models import Account, Notification, Transaction
|
||||
from ..utils.iban import check_iban
|
||||
from .. import decorators, returns
|
||||
|
||||
bp = Blueprint('transactions', __name__, description='Bank transfers and other transactions')
|
||||
|
||||
@bp.route('/')
|
||||
class TransactionsList(MethodView):
|
||||
class TransactionsParams(Schema):
|
||||
account_id = fields.Int(min=1)
|
||||
|
||||
class TransactionsGetResponse(returns.SuccessSchema):
|
||||
transactions = fields.List(fields.Nested(Transaction.TransactionSchema))
|
||||
|
||||
@ensure_logged_in
|
||||
@bp.response(401, returns.ErrorSchema, description='Login failure or not allowed')
|
||||
@bp.doc(security=[{'Token': []}])
|
||||
@bp.arguments(TransactionsParams, as_kwargs=True, location='query')
|
||||
@bp.response(200, TransactionsGetResponse)
|
||||
def get(self, account_id: int):
|
||||
"""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(
|
||||
transactions=get_transactions(account_id)
|
||||
)
|
||||
|
||||
class TransactionsCreateParams(Schema):
|
||||
account_id = fields.Int(min=1)
|
||||
destination_iban = fields.Str()
|
||||
amount = fields.Int(min=1)
|
||||
description = fields.Str(default='')
|
||||
|
||||
class TransactionsCreateResponse(returns.SuccessSchema):
|
||||
transaction = fields.Nested(Transaction.TransactionSchema)
|
||||
|
||||
@ensure_logged_in
|
||||
@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.arguments(TransactionsCreateParams, as_kwargs=True)
|
||||
@bp.response(200, TransactionsCreateResponse)
|
||||
def post(self, account_id: int, destination_iban: str, amount: int, description: str = ''):
|
||||
"""Create a send_transfer transaction"""
|
||||
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,
|
||||
'description': description,
|
||||
},
|
||||
)
|
||||
insert_transaction(acc.id, reverse_transaction)
|
||||
formatted_iban = re.sub(r'(.{4})', r'\1 ', account.iban).strip()
|
||||
notification = Notification.new_notification(
|
||||
body=f'Transfer of {-amount // 100}.{-amount % 100:0>2} {account.currency} received from {formatted_iban} in your {acc.custom_name or acc.account_type} account.',
|
||||
date_time=date,
|
||||
read=False,
|
||||
)
|
||||
insert_notification(acc.id, notification)
|
||||
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
|
||||
import json
|
||||
import sys
|
||||
from types import ModuleType
|
||||
|
||||
|
@ -85,7 +86,19 @@ class Module(ModuleType):
|
|||
''', (user_id,))
|
||||
else:
|
||||
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
|
||||
|
@ -106,7 +119,18 @@ class Module(ModuleType):
|
|||
result = cur.fetchone()
|
||||
if result is 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
|
||||
|
@ -158,4 +182,129 @@ class Module(ModuleType):
|
|||
|
||||
self.db.commit()
|
||||
|
||||
@get_db
|
||||
def get_transactions(self, account_id: int) -> list[models.Transaction]:
|
||||
cur = self.db.cursor()
|
||||
cur.execute(
|
||||
'select transaction_id from accounts_transactions where account_id = ?',
|
||||
(account_id,),
|
||||
)
|
||||
|
||||
transactions = []
|
||||
for tid in (row['transaction_id'] for row in cur.fetchall()):
|
||||
cur.execute(
|
||||
'select * from transactions where id = ?',
|
||||
(tid,),
|
||||
)
|
||||
|
||||
db_res = cur.fetchone()
|
||||
if db_res is None:
|
||||
continue
|
||||
transactions.append(models.Transaction.from_query(db_res))
|
||||
|
||||
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()
|
||||
|
||||
@get_db
|
||||
def get_notifications(self, user_id: int) -> list[models.Notification]:
|
||||
cur = self.db.cursor()
|
||||
|
||||
cur.execute(
|
||||
'''
|
||||
select n.id, n.body, n.datetime, n.read
|
||||
from notifications as n
|
||||
inner join users_notifications on n.id = users_notifications.notification_id
|
||||
where users_notifications.user_id = ?
|
||||
''',
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
return [models.Notification.from_query(q) for q in cur.fetchall()]
|
||||
|
||||
@get_db
|
||||
def insert_notification(self, user_id: int, notification: models.Notification):
|
||||
cur = self.db.cursor()
|
||||
|
||||
cur.execute(
|
||||
'insert into notifications(body, datetime, read) values (?, ?, ?)',
|
||||
(
|
||||
notification.body,
|
||||
notification.date_time.isoformat(),
|
||||
1 if notification.read else 0,
|
||||
),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
'select id from notifications where body = ? and datetime = ? and read = ?',
|
||||
(
|
||||
notification.body,
|
||||
notification.date_time.isoformat(),
|
||||
1 if notification.read else 0,
|
||||
),
|
||||
)
|
||||
notification.id = cur.fetchone()['id']
|
||||
|
||||
cur.execute(
|
||||
'insert into users_notifications values (?, ?)',
|
||||
(user_id, notification.id,),
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
@get_db
|
||||
def whose_notification(self, notification: int | models.Notification) -> int | None:
|
||||
try:
|
||||
notification_id = notification.id
|
||||
except AttributeError:
|
||||
notification_id = notification
|
||||
|
||||
cur = self.db.cursor()
|
||||
cur.execute('select user_id from users_notifications where notification_id = ?', (notification_id,))
|
||||
result = cur.fetchone()
|
||||
if not result:
|
||||
return None
|
||||
return result[0]
|
||||
|
||||
@get_db
|
||||
def mark_notification_as_read(self, notification_id: int):
|
||||
cur = self.db.cursor()
|
||||
cur.execute(
|
||||
'update notifications set read = 1 where id = ?',
|
||||
(notification_id,),
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
|
||||
sys.modules[__name__] = Module(__name__)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from marshmallow import Schema, fields
|
||||
from datetime import datetime
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
|
@ -50,13 +51,15 @@ class Account:
|
|||
currency: str
|
||||
account_type: str
|
||||
custom_name: str
|
||||
balance: int = field(default=0)
|
||||
|
||||
class Schema(Schema):
|
||||
class AccountSchema(Schema):
|
||||
id = fields.Int(required=False)
|
||||
iban = fields.Str()
|
||||
currency = fields.Str()
|
||||
account_type = fields.Str(data_key='accountType')
|
||||
custom_name = fields.Str(data_key='customName')
|
||||
balance = fields.Int()
|
||||
|
||||
@staticmethod
|
||||
def new_account(currency: str, account_type: str, custom_name: str = '') -> 'Account':
|
||||
|
@ -74,6 +77,7 @@ class Account:
|
|||
'currency': self.currency,
|
||||
'accountType': self.account_type,
|
||||
'customName': self.custom_name,
|
||||
'balance': self.balance,
|
||||
}
|
||||
if include_id:
|
||||
result['id'] = self.id
|
||||
|
@ -82,3 +86,90 @@ class Account:
|
|||
@classmethod
|
||||
def from_query(cls, query_result):
|
||||
return cls(*query_result)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
id: int
|
||||
date_time: datetime
|
||||
other_party: str
|
||||
status: str
|
||||
transaction_type: str
|
||||
extra: str
|
||||
|
||||
class TransactionSchema(Schema):
|
||||
id = fields.Int(required=False)
|
||||
date_time = fields.DateTime(data_key='datetime')
|
||||
other_party = fields.Dict(keys=fields.Str(), values=fields.Raw(), data_key='otherParty')
|
||||
status = fields.Str()
|
||||
transaction_type = fields.Str(data_key='transactionType')
|
||||
extra = fields.Dict(keys=fields.Str(), values=fields.Raw())
|
||||
|
||||
@staticmethod
|
||||
def new_transaction(date_time: datetime, other_party: str, status: str, transaction_type: str, extra: str = '') -> 'Transaction':
|
||||
return Transaction(
|
||||
id=-1,
|
||||
date_time=date_time,
|
||||
other_party=other_party,
|
||||
status=status,
|
||||
transaction_type=transaction_type,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def to_json(self, include_id=True):
|
||||
result = {
|
||||
'datetime': self.date_time.isoformat(),
|
||||
'otherParty': self.other_party,
|
||||
'status': self.status,
|
||||
'transactionType': self.transaction_type,
|
||||
'extra': self.extra,
|
||||
}
|
||||
if include_id:
|
||||
result['id'] = self.id
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
id: int
|
||||
body: str
|
||||
date_time: datetime
|
||||
read: bool
|
||||
|
||||
class NotificationSchema(Schema):
|
||||
id = fields.Int(required=False)
|
||||
body = fields.Str()
|
||||
date_time = fields.DateTime(data_key='datetime')
|
||||
read = fields.Bool()
|
||||
|
||||
@staticmethod
|
||||
def new_notification(body: str, date_time: datetime, read: bool = False) -> 'Notification':
|
||||
return Notification(
|
||||
id=-1,
|
||||
body=body,
|
||||
date_time=date_time,
|
||||
read=read,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_query(cls, query_result):
|
||||
query_result = list(query_result)
|
||||
if type(query_result[2]) is str:
|
||||
query_result[2] = datetime.fromisoformat(query_result[2])
|
||||
if type(query_result[3]) is not bool:
|
||||
query_result[3] = bool(query_result[3])
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -74,15 +88,15 @@ def success(http_status: Any = _HTTPStatus.OK, /, **kargs):
|
|||
|
||||
# Schemas
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
from marshmallow import Schema, fields, validate
|
||||
|
||||
class ErrorSchema(Schema):
|
||||
status = fields.Constant('error')
|
||||
status = fields.Str(default='error', validate=validate.Equal('error'))
|
||||
code = fields.Str()
|
||||
message = fields.Str(required=False)
|
||||
|
||||
class SuccessSchema(Schema):
|
||||
status = fields.Constant('success')
|
||||
status = fields.Str(default='success', validate=validate.Equal('success'))
|
||||
|
||||
# smorest
|
||||
|
||||
|
|
|
@ -1,6 +1,30 @@
|
|||
from .string import str_range_replace
|
||||
|
||||
|
||||
IBAN_BANKS = {
|
||||
'RO': {
|
||||
'NBOR': 'BANCA NATIONALA A ROMANIEI',
|
||||
'BUCU': 'ALPHA BANK ROMANIA SA',
|
||||
'CARP': 'BANCA COMERCIALA CARPATICA SA',
|
||||
'RNCB': 'BANCA COMERCIALA ROMANA SA',
|
||||
'BRDE': 'BANCA ROMANA PENTRU DEZVOLTARE',
|
||||
'BRMA': 'BANCA ROMANEASCA SA',
|
||||
'BTRL': 'BANCA TRANSILVANIA SA',
|
||||
'DAFB': 'BANK LEUMI ROMANIA SA',
|
||||
'CECE': 'CASA DE ECONOMII SI CONSEMNATIUNI CEC SA',
|
||||
'CITI': 'CITIBANK ROMANIA SA',
|
||||
'UGBI': 'GARANTIBANK INTERNATIONAL NV - SUCURSALA ROMANIA',
|
||||
'INGB': 'ING BANK NV',
|
||||
'BREL': 'LIBRA BANK SA',
|
||||
'BNRB': 'OTP BANK ROMANIA SA',
|
||||
'RZBR': 'RAIFFEISEN BANK SA',
|
||||
'TREZ': 'TREZORERIA STATULUI',
|
||||
'BACX': 'UNICREDIT BANK SA',
|
||||
'FOXB': 'FOXBANK',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def c_to_iban_i(c: str) -> int:
|
||||
a = ord(c)
|
||||
if a in range(48, 58):
|
||||
|
|
|
@ -59,3 +59,11 @@ create table users_notifications (
|
|||
foreign key (user_id) references users (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