mirror of
				https://github.com/dancojocaru2000/foxbank.git
				synced 2025-10-31 15:36:32 +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