diff --git a/server/.vscode/launch.json b/server/.vscode/launch.json index d9be6a0..195ea05 100644 --- a/server/.vscode/launch.json +++ b/server/.vscode/launch.json @@ -11,7 +11,8 @@ "module": "flask", "env": { "FLASK_APP": "server.py", - "FLASK_ENV": "development" + "FLASK_ENV": "development", + "FLASK_RUN_PORT": "5001" }, "args": [ "run", diff --git a/server/foxbank_server/apis/__init__.py b/server/foxbank_server/apis/__init__.py index e1d307b..f5d1919 100644 --- a/server/foxbank_server/apis/__init__.py +++ b/server/foxbank_server/apis/__init__.py @@ -3,6 +3,7 @@ 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 class ApiWithErr(Api): def handle_http_exception(self, error): @@ -27,3 +28,4 @@ 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') diff --git a/server/foxbank_server/apis/accounts.py b/server/foxbank_server/apis/accounts.py index 9b4017e..920f7d6 100644 --- a/server/foxbank_server/apis/accounts.py +++ b/server/foxbank_server/apis/accounts.py @@ -13,12 +13,10 @@ 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') @bp.get('/meta/currencies') @@ -36,7 +34,7 @@ def get_valid_account_types(): class AccountResponseSchema(returns.SuccessSchema): - account = fields.Nested(Account.Schema) + account = fields.Nested(Account.AccountSchema) @bp.get('/') @@ -79,7 +77,7 @@ class AccountsList(MethodView): custom_name = fields.String(data_key='customName') 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') @@ -99,7 +97,7 @@ class AccountsList(MethodView): return returns.success(account=account.to_json()) 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') diff --git a/server/foxbank_server/apis/transactions.py b/server/foxbank_server/apis/transactions.py new file mode 100644 index 0000000..7e29624 --- /dev/null +++ b/server/foxbank_server/apis/transactions.py @@ -0,0 +1,46 @@ +from flask.views import MethodView +from flask_smorest import Blueprint +from marshmallow import Schema, fields + +from ..decorators import ensure_logged_in +from ..models import Transaction +from ..db_utils import get_transactions +from .. import 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') + @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""" + return returns.success( + transactions=[t.to_json() for t in get_transactions(account_id)] + ) + + class TransactionsCreateParams(Schema): + account_id = fields.Int(min=1) + destination_iban = fields.Str() + amount = fields.Int(min=1) + + class TransactionsCreateResponse(returns.SuccessSchema): + transaction = fields.Nested(Transaction.TransactionSchema) + + @ensure_logged_in + @bp.response(401, returns.ErrorSchema, description='Login failure') + @bp.doc(security=[{'Token': []}]) + @bp.arguments(TransactionsCreateParams) + @bp.response(200, TransactionsCreateResponse) + def post(self, account_id: int, destination_iban: str, amount: int): + """Create a send_transfer transaction""" + raise NotImplementedError() diff --git a/server/foxbank_server/db_utils.py b/server/foxbank_server/db_utils.py index 3170413..0f4c240 100644 --- a/server/foxbank_server/db_utils.py +++ b/server/foxbank_server/db_utils.py @@ -158,4 +158,26 @@ 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 + sys.modules[__name__] = Module(__name__) diff --git a/server/foxbank_server/models.py b/server/foxbank_server/models.py index 0e01f7a..8d89a82 100644 --- a/server/foxbank_server/models.py +++ b/server/foxbank_server/models.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from marshmallow import Schema, fields +from datetime import datetime @dataclass class User: @@ -51,7 +52,7 @@ class Account: account_type: str custom_name: str - class Schema(Schema): + class AccountSchema(Schema): id = fields.Int(required=False) iban = fields.Str() currency = fields.Str() @@ -82,3 +83,47 @@ 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.Str(data_key='otherParty') + transaction_type = fields.Str(data_key='transactionType') + extra = fields.Str() + + @staticmethod + def new_transaction(date_time: datetime, other_party: str, status: str, transaction_type: str, extra: str = '') -> 'Account': + 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): + return cls(*query_result) diff --git a/server/foxbank_server/returns.py b/server/foxbank_server/returns.py index f78f852..69bf550 100644 --- a/server/foxbank_server/returns.py +++ b/server/foxbank_server/returns.py @@ -74,15 +74,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