Browse Source

Implement personal loans

Eren Yilmaz 5 years ago
parent
commit
8f00c9d558
12 changed files with 337 additions and 77 deletions
  1. 15 14
      .gitignore
  2. 104 9
      client_controller.py
  3. 1 1
      connection.py
  4. 9 9
      db_setup/create_triggers.py
  5. 4 0
      db_setup/indices.py
  6. 10 0
      db_setup/tables.py
  7. 1 0
      game.py
  8. 110 8
      model.py
  9. 30 0
      routes.py
  10. 5 25
      run_client.py
  11. 3 7
      run_server.py
  12. 45 4
      server_controller.py

+ 15 - 14
.gitignore

@@ -1,14 +1,15 @@
-.idea
-*.db
-venv
-*.db-journal
-*.spec
-build
-dist
-cool_query.py
-__pycache__
-future_plans.*
-secret_trading_tools.py
-*.bak*
-orderer.zip
-*.db-*
+.idea
+*.db
+venv
+*.db-journal
+*.spec
+build
+dist
+cool_query.py
+__pycache__
+future_plans.*
+secret_trading_tools.py
+*.bak*
+orderer.zip
+*.db-*
+logs/*.py.html

+ 104 - 9
client_controller.py

@@ -5,8 +5,9 @@ from inspect import signature
 import connection
 from connection import client_request
 from debug import debug
-from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME
-from run_client import allowed_commands, fake_loading_bar
+from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME, MIN_INTEREST_INTERVAL
+from routes import client_commands
+from run_client import fake_loading_bar
 from util import my_tabulate, yn_dialog
 
 exiting = False
@@ -128,7 +129,7 @@ def change_pw(password=None, retype_pw=None):
 def help():
     print('Allowed commands:')
     command_table = []
-    for cmd in allowed_commands:
+    for cmd in client_commands:
         this_module = sys.modules[__name__]
         method = getattr(this_module, cmd)
         params = signature(method).parameters
@@ -159,7 +160,8 @@ def depot():
             row.append(row[1] * row[2])
         print(my_tabulate(data,
                           headers=['Object', 'Amount', 'Course', 'Bid', 'Ask', 'Est. Value'],
-                          tablefmt="pipe"))
+                          tablefmt="pipe",
+                          floatfmt='.2f'))
         wealth = response['own_wealth']
         print(f'This corresponds to a wealth of roughly {wealth}.')
         if response['banking_license']:
@@ -296,10 +298,11 @@ def buy_banking_license():
     fake_loading_bar('Filling the necessary forms', duration=14.4)
     fake_loading_bar('Waiting for bank regulation\'s response', duration=26.8)
     response = client_request('buy_banking_license', {"session_id": connection.session_id})
-    success = 'data' in response
+    success = 'message' in response and 'error' not in response
     if success:
         print('Success. You are now a bank.')
-        summarize_banking_rules()
+        print()
+        summarize_bank_rules()
         # print('Remember that the goal is to be as rich as possible, not to be richer than other traders!')
     else:
         if 'error' in response:
@@ -308,15 +311,107 @@ def buy_banking_license():
             print('Banking license application access failed.')
 
 
-def summarize_banking_rules():
+def summarize_bank_rules():
     variables = _global_variables()
+    banking_license_price = variables['banking_license_price']
     marginal_lending_facility = variables['marginal_lending_facility']
     cash_reserve_free_amount = variables['cash_reserve_free_amount']
     cash_reserve_ratio = variables['cash_reserve_ratio']
-    print(f'AS a bank, you are now allowed to borrow money from the central bank at the marginal '
-          f'lending facility (currently {marginal_lending_facility * 100}%).')
+    deposit_facility = variables['deposit_facility']
+    print(f'A bank is by definition anyone who has a banking license.')
+    print(f'A banking license can be for {banking_license_price} {CURRENCY_NAME}.')
+    print(f'This includes payment of lawyers and consultants to deal with the formal application.')
+    print()
+    print(f'Banks are allowed to borrow money from the central bank at the marginal '
+          f'lending facility (currently {marginal_lending_facility * 100}% p.a.).')
     print(f'For every {CURRENCY_NAME} above {cash_reserve_free_amount} banks have to '
           f'deposit a cash reserve of {cash_reserve_ratio * 100}% at the central bank.')
+    print(f'Banks receive a deposit facility of {deposit_facility * 100}% p.a. for cash reserves.')
+
+
+def summarize_loan_rules():
+    variables = _global_variables()
+    personal_loan_interest_rate = variables['personal_loan_interest_rate']
+    print(f'You can take personal loans at an interest rate of {personal_loan_interest_rate * 100}%  p.a.')
+    print(f'You can repay personal loans at any time if you have the liquidity.')
+    print(f'Interest rates will be automatically be debited from your account.')
+    print(f'Note that this debit may be delayed by up to {MIN_INTEREST_INTERVAL} seconds.')
+    print(f'If you have no {CURRENCY_NAME} available (or negative account balance), you can still')
+    print(f' - pay any further interests (account will move further into the negative)')
+    print(f' - take a new loan (if we think that you will be able to repay it)')
+    print(f' - sell any securities you own')
+    print(f'However you can not')
+    print(f' - spend money to buy securities')
+    print(f' - spend money to buy a banking license')
+
+
+def take_out_personal_loan(amount=None):
+    if amount is None:
+        summarize_loan_rules()
+        print()
+        amount = input('Please enter your desired loan volume: ')
+    try:
+        amount = float(amount)
+    except ValueError:
+        print('Amount must be a number larger than 0.')
+        return
+    if amount <= 0:
+        print('Amount must be a number larger than 0.')
+    fake_loading_bar('Checking if you are trustworthy', duration=0.0)
+    fake_loading_bar('Checking if you are credit-worthy', duration=0.0)
+    fake_loading_bar('Transferring the money', duration=1.6)
+    response = client_request('take_out_personal_loan', {"session_id": connection.session_id, 'amount': amount})
+    success = 'message' in response and 'error' not in response
+    if success:
+        print(f'You took a personal loan of {amount} {CURRENCY_NAME}.')
+    else:
+        if 'error' in response:
+            print('Taking a personal loan failed with message:', response['error'])
+        else:
+            print('Taking a personal loan failed.')
+
+
+def loans():
+    fake_loading_bar('Loading Data', duration=0.9)
+    response = client_request('loans', {"session_id": connection.session_id})
+    success = 'data' in response
+    if success:
+        print(my_tabulate(response['data'],
+                          headers=['Loan ID', 'Total amount', 'Remaining', 'Interest p.a.', ],
+                          tablefmt="pipe"))
+    else:
+        if 'error' in response:
+            print('Order access failed with message:', response['error'])
+        else:
+            print('Order access failed.')
+
+
+def repay_loan(loan_id=None, amount=None):
+    if loan_id is None:
+        loans()
+        print('Which loan would you like to pay back?')
+        loan_id = input('Loan id:')
+    if amount is None:
+        print('How much would you like to pay back?')
+        amount = input('Amount (type `all` for the remaining loan):')
+    if amount != 'all':
+        try:
+            amount = float(amount)  # this can also raise a ValueError
+            if amount <= 0:
+                raise ValueError
+        except ValueError:
+            print('Amount must be a number larger than 0 or \'all\' (for paying back the remaining loan).')
+            return
+    fake_loading_bar('Transferring the money', duration=1.7)
+    response = client_request('repay_loan', {"session_id": connection.session_id, 'amount': amount, 'loan_id': loan_id})
+    success = 'message' in response and 'error' not in response
+    if success:
+        print(f'You repayed the specified amount of {CURRENCY_NAME}.')
+    else:
+        if 'error' in response:
+            print('Repaying the loan failed with message:', response['error'])
+        else:
+            print('Repaying the loan failed.')
 
 
 def _global_variables():

+ 1 - 1
connection.py

@@ -101,7 +101,7 @@ def check_missing_attributes(request_json: Dict, attributes: List[str]):
             raise BadRequest('Missing value for attribute ' + str(attr))
         if str(attr) == 'session_id':
             if not model.valid_session_id(request_json['session_id']):
-                raise Unauthorized('Invalid value for attribute ' + str(attr))
+                raise Unauthorized('You are not signed in.')
 
 
 def client_request(route, data=None):

+ 9 - 9
db_setup/create_triggers.py

@@ -1,24 +1,24 @@
 import sqlite3
 from typing import List
 
-from game import MINIMUM_ORDER_AMOUNT
+from game import MINIMUM_ORDER_AMOUNT, CURRENCY_NAME
 
 
 def create_triggers(cursor: sqlite3.Cursor):
     print(' - Creating triggers...')
     # ensure that the internal rowids of any table are not updated after creation
     create_triggers_that_restrict_rowid_update(cursor)
-    cursor.execute('''
+    cursor.execute(f'''
                 CREATE TRIGGER IF NOT EXISTS owned_amount_not_negative_after_insert
                 AFTER INSERT ON ownership
-                WHEN NEW.amount < 0
-                BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0.'); END
+                WHEN NEW.amount < 0 AND (SELECT name FROM ownables WHERE ownables.rowid = NEW.ownable_id) != {CURRENCY_NAME}
+                BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0 (except {CURRENCY_NAME}).'); END
                 ''')
-    cursor.execute('''
+    cursor.execute(f'''
                 CREATE TRIGGER IF NOT EXISTS owned_amount_not_negative_after_update
                 AFTER UPDATE ON ownership
-                WHEN NEW.amount < 0
-                BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0.'); END
+                WHEN NEW.amount < 0 AND (SELECT name FROM ownables WHERE ownables.rowid = NEW.ownable_id) != {CURRENCY_NAME}
+                BEGIN SELECT RAISE(ROLLBACK, 'Can not own an amount less than 0 (except {CURRENCY_NAME}).'); END
                 ''')
     cursor.execute('''
                 CREATE TRIGGER IF NOT EXISTS amount_positive_after_insert
@@ -239,7 +239,7 @@ def create_combination_cluster_triggers(cursor: sqlite3.Cursor,
     '''.format(table_name, kind_column_name, valid_kind))
     for referenced_table in referenced_tables:
         cursor.execute('''-- noinspection SqlResolveForFile
-            CREATE TRIGGER {0}_{1}_{3}_foreign_key_before_delete
+            CREATE TRIGGER IF NOT EXISTS {0}_{1}_{3}_foreign_key_before_delete
             BEFORE DELETE ON {3}
             WHEN EXISTS (
                 SELECT * FROM {0}
@@ -257,7 +257,7 @@ def create_triggers_that_restrict_rowid_update(cursor):
     tables = [row[0] for row in cursor.fetchall()]
     for table_name in tables:
         cursor.execute('''-- noinspection SqlResolveForFile
-                    CREATE TRIGGER restrict_rowid_update_on_{0}
+                    CREATE TRIGGER IF NOT EXISTS restrict_rowid_update_on_{0}
                     AFTER UPDATE ON {0}
                     WHEN OLD.rowid <> NEW.rowid
                     BEGIN SELECT RAISE(ROLLBACK, 'The rowid can not be changed.'); END

+ 4 - 0
db_setup/indices.py

@@ -67,4 +67,8 @@ def create_indices(cursor: Cursor):
     cursor.execute('''
                 CREATE INDEX IF NOT EXISTS order_history_ownership
                 ON order_history (ownership_id)
+                ''')
+    cursor.execute('''
+                CREATE INDEX IF NOT EXISTS loan_by_user
+                ON loans (user_id, last_interest_pay_dt)
                 ''')

+ 10 - 0
db_setup/tables.py

@@ -109,6 +109,16 @@ def tables(cursor):
                     UNIQUE (value_name, dt)
                 )
                 ''')
+    cursor.execute('''
+                CREATE TABLE IF NOT EXISTS loans(
+                    rowid INTEGER PRIMARY KEY,
+                    user_id INTEGER NOT NULL REFERENCES users(rowid),
+                    total_amount CURRENCY NOT NULL, -- TODO trigger that total amount is strictly larger 0
+                    amount CURRENCY NOT NULL, -- TODO trigger that amount is strictly larger 0
+                    last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    interest_rate CURRENCY NOT NULL -- determined from the global value 'personal_loan_interest_rate'
+                )
+                ''')
     _add_column_if_not_exists(cursor, '''
                 -- there is a not null constraint for new values that is watched by triggers
                 ALTER TABLE users ADD COLUMN salt BLOB NOT NULL DEFAULT 'orderer_is_a_cool_application_]{][{²$%WT§$%GV§$%SF$%&S$%FGGFHBDHJZIF254325'

+ 1 - 0
game.py

@@ -5,6 +5,7 @@ MINIMUM_ORDER_AMOUNT = 1
 DEFAULT_ORDER_EXPIRY = 43200
 DB_NAME = 'orderer'
 COPYRIGHT_INFRINGEMENT_PROBABILITY = 0.05
+MIN_INTEREST_INTERVAL = 60  # seconds
 ROOT_URL = "/orderer.zip"
 
 logger = DBLog()

+ 110 - 8
model.py

@@ -13,7 +13,7 @@ from passlib.handlers.sha2_crypt import sha256_crypt
 
 import db_setup
 import trading_bot
-from game import CURRENCY_NAME, logger, DB_NAME
+from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL
 from util import random_chars
 
 DBName = str
@@ -30,6 +30,12 @@ def execute(sql, parameters=()):
     return current_cursor.execute(sql, parameters)
 
 
+def executemany(sql, parameters=()):
+    if not re.search(r"(?i)\s*SELECT", sql):
+        logger.info(sql, 'sql_query_many', data=json.dumps(parameters))
+    return current_cursor.executemany(sql, parameters)
+
+
 def valid_db_name(name):
     return re.match(r"[a-z0-9.-]{0,20}", name)
 
@@ -178,12 +184,13 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
     if amount < 0:
         raise AssertionError('Can not send negative amount')
 
-    execute('''
-                UPDATE ownership
-                SET amount = amount - ?
-                WHERE user_id = ?
-                AND ownable_id = ?
-                ''', (amount, from_user_id, ownable_id,))
+    if from_user_id != bank_id():
+        execute('''
+                    UPDATE ownership
+                    SET amount = amount - ?
+                    WHERE user_id = ?
+                    AND ownable_id = ?
+                    ''', (amount, from_user_id, ownable_id,))
 
     own(to_user_id, ownable_name_by_id(ownable_id))
 
@@ -322,7 +329,7 @@ def get_user_ownership(user_id):
              AND NOT stop_loss) AS ask
         FROM ownership, ownables
         WHERE user_id = ?
-        AND (ownership.amount > 0 OR ownership.ownable_id = ?)
+        AND (ownership.amount >= 0.01 OR ownership.ownable_id = ?)
         AND ownership.ownable_id = ownables.rowid
         ORDER BY ownables.rowid ASC
         ''', (currency_id(), user_id, currency_id(),))
@@ -398,6 +405,23 @@ def get_user_orders(user_id):
     return current_cursor.fetchall()
 
 
+def get_user_loans(user_id):
+    connect()
+
+    execute('''
+        SELECT 
+            rowid, 
+            total_amount,
+            amount,
+            interest_rate
+        FROM loans
+        WHERE user_id is ?
+        ORDER BY rowid ASC
+        ''', (user_id,))
+
+    return current_cursor.fetchall()
+
+
 def get_ownable_orders(user_id, ownable_id):
     connect()
 
@@ -1260,3 +1284,81 @@ def assign_banking_licence(user_id):
         INSERT INTO banks(user_id)
         VALUES (?)
         ''', (user_id,))
+
+
+def pay_loan_interest():
+    current_dt = execute("SELECT strftime('%s', CURRENT_TIMESTAMP)").fetchone()[0]
+    sec_per_year = 3600 * 24 * 365
+    interests = execute('''
+    SELECT 
+        SUM(amount * (POWER(1 + interest_rate, 
+                            (CAST(? AS FLOAT) - last_interest_pay_dt) / ?) - 1)
+            ) AS interest_since_last_pay,
+        user_id
+    FROM loans
+    WHERE ? - last_interest_pay_dt > ?
+    GROUP BY user_id
+    ''', (current_dt, sec_per_year, current_dt, MIN_INTEREST_INTERVAL)).fetchall()
+    executemany(f'''
+    UPDATE ownership
+    SET amount = amount - ?
+    WHERE ownable_id = {currency_id()}
+    AND user_id = ?
+    ''', interests)
+    # noinspection SqlWithoutWhere
+    execute('''
+    UPDATE loans
+    SET last_interest_pay_dt = ?
+    WHERE ? - last_interest_pay_dt > ?
+    ''', (current_dt, current_dt, MIN_INTEREST_INTERVAL,))
+
+
+def loan_recipient_id(loan_id):
+    execute('''
+        SELECT user_id
+        FROM loans
+        WHERE rowid = ?
+        ''', (loan_id,))
+
+    return current_cursor.fetchone()[0]
+
+
+def loan_remaining_amount(loan_id):
+    execute('''
+        SELECT amount
+        FROM loans
+        WHERE rowid = ?
+        ''', (loan_id,))
+
+    return current_cursor.fetchone()[0]
+
+
+def repay_loan(loan_id, amount, known_user_id=None):
+    if known_user_id is None:
+        user_id = loan_recipient_id(loan_id)
+    else:
+        user_id = known_user_id
+    send_ownable(user_id, bank_id(), currency_id(), amount)
+
+    execute('''
+        UPDATE loans
+        SET amount = amount - ?
+        WHERE rowid = ?
+        ''', (amount, loan_id,))
+
+
+def take_out_personal_loan(user_id, amount):
+    execute('''
+        INSERT INTO loans(user_id, total_amount, amount, interest_rate) 
+        VALUES (?, ?, ?, ?)
+        ''', (user_id, amount, amount, global_control_value('personal_loan_interest_rate')))
+
+    send_ownable(bank_id(), user_id, currency_id(), amount)
+
+
+def loan_id_exists(loan_id):
+    execute('''
+        SELECT EXISTS (SELECT * FROM loans WHERE rowid = ?)
+        ''', (loan_id,))
+
+    return current_cursor.fetchone()[0]

+ 30 - 0
routes.py

@@ -17,6 +17,9 @@ valid_post_routes = {
     'change_password',
     'global_variables',
     'buy_banking_license',
+    'take_out_personal_loan',
+    'repay_loan',
+    'loans',
 }
 
 push_message_types = set()
@@ -25,3 +28,30 @@ upload_filtered = set()  # TODO enable upload filter again when accuracy improve
 
 assert len(set(valid_post_routes)) == len(valid_post_routes)
 assert upload_filtered.issubset(valid_post_routes)
+
+# in the order in that they will be displayed by the help command
+client_commands = ['help',
+                   'summarize_bank_rules',
+                   'summarize_loan_rules',
+                   'login',
+                   'register',
+                   'change_pw',
+                   'news',
+                   'tradables',
+                   'take_out_personal_loan',
+                   'repay_loan',
+                   'loans',
+                   'depot',
+                   'orders',
+                   'orders_on',
+                   'old_orders',
+                   'trades',
+                   'trades_on',
+                   'buy',
+                   'sell',
+                   'cancel_order',
+                   'gift',
+                   'leaderboard',
+                   'activate_key',
+                   'exit',
+                   'buy_banking_license', ]

+ 5 - 25
run_client.py

@@ -3,9 +3,12 @@ from __future__ import print_function
 import sys
 import time
 
+import requests
+
 import client_controller
 from debug import debug
 from lib.print_exc_plus import print_exc_plus
+from routes import client_commands
 from util import yn_dialog, main_wrapper
 
 
@@ -54,29 +57,6 @@ To display an overview of available commands type 'help'.
 ''')
 
 
-allowed_commands = ['help',
-                    'login',
-                    'register',
-                    'change_pw',
-                    'news',
-                    'tradables',
-                    'depot',
-                    'orders',
-                    'orders_on',
-                    'old_orders',
-                    'trades',
-                    'trades_on',
-                    'buy',
-                    'sell',
-                    'cancel_order',
-                    'gift',
-                    'leaderboard',
-                    'activate_key',
-                    'exit',
-                    'summarize_banking_rules',
-                    'buy_banking_license']
-
-
 def one_command():
     try:
         cmd = input('*> ').strip()
@@ -94,7 +74,7 @@ def one_command():
         if cmd == []:
             continue
         cmd[0] = cmd[0].lower()
-        if cmd[0] not in allowed_commands:
+        if cmd[0] not in client_commands:
             print('Invalid command:', cmd[0])
         else:
             method_to_call = getattr(client_controller, cmd[0])
@@ -103,7 +83,7 @@ def one_command():
                 method_to_call(*cmd[1:])
             except TypeError:
                 print('Invalid command syntax.')
-            except ConnectionError:
+            except (ConnectionError, requests.exceptions.ConnectionError):
                 print('There has been a problem connecting when to the server.')
             except KeyboardInterrupt:
                 print('Interrupted')

+ 3 - 7
run_server.py

@@ -31,7 +31,7 @@ from game import ROOT_URL, COPYRIGHT_INFRINGEMENT_PROBABILITY, DB_NAME, logger
 from lib.print_exc_plus import print_exc_plus
 from lib.threading_timer_decorator import exit_after
 from routes import valid_post_routes, upload_filtered
-from util import round_to_n, rename, profile_wall_time_instead_if_profiling, LogicError
+from util import round_to_n, rename, profile_wall_time_instead_if_profiling
 
 FRONTEND_RELATIVE_PATH = './frontend'
 
@@ -82,13 +82,9 @@ def _process(path, json_request):
             resp = connection.NotFound('URL not available')
         else:
             model.connect(DB_NAME, create_if_not_exists=True)
-            if path not in valid_post_routes:
-                if not debug:
-                    raise LogicError
-                method_to_call = server_controller._mocked_route
-            else:
-                method_to_call = getattr(server_controller, path)
+            method_to_call = getattr(server_controller, path)
             try:
+                server_controller.before_request(json_request)
                 resp = call_controller_method_with_timeout(method_to_call, json_request)
                 if isinstance(resp, HttpError):
                     raise resp

+ 45 - 4
server_controller.py

@@ -6,7 +6,7 @@ from bottle import request
 from passlib.hash import sha256_crypt
 
 import model
-from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed
+from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound
 
 
 def missing_attributes(attributes):
@@ -37,8 +37,8 @@ def depot(json_request):
     check_missing_attributes(json_request, ['session_id'])
     user_id = model.get_user_id_by_session_id(request.json['session_id'])
     return {'data': model.get_user_ownership(user_id),
-            'own_wealth': model.user_wealth(user_id),
-            'banking_licence': model.user_has_banking_license(user_id)}
+            'own_wealth': f'{model.user_wealth(user_id):.2f}',
+            'banking_license': model.user_has_banking_license(user_id)}
 
 def global_variables(_json_request):
     return model.global_control_values()
@@ -178,6 +178,12 @@ def orders(json_request):
     return {'data': data}
 
 
+def loans(json_request):
+    check_missing_attributes(json_request, ['session_id'])
+    data = model.get_user_loans(model.get_user_id_by_session_id(request.json['session_id']))
+    return {'data': data}
+
+
 def orders_on(json_request):
     check_missing_attributes(json_request, ['session_id', 'ownable'])
     if not model.ownable_name_exists(request.json['ownable']):
@@ -225,7 +231,7 @@ def buy_banking_license(json_request):
         raise PreconditionFailed('You do not have enough money.')
     model.send_ownable(user_id, model.bank_id(), model.currency_id(), price)
     model.assign_banking_licence(user_id)
-    return {'message': "Successfully bought banking licencse"}
+    return {'message': "Successfully bought banking license"}
 
 
 def news(_json_request):
@@ -250,3 +256,38 @@ def trades_on(json_request):
 
 def leaderboard(_json_request):
     return {'data': model.leaderboard()}
+
+
+def take_out_personal_loan(json_request):
+    check_missing_attributes(json_request, ['session_id', 'amount', ])
+    amount = json_request['amount']
+    if not isinstance(amount, float) or amount <= 0:
+        raise BadRequest('Amount must be a number larger than 0')
+    user_id = model.get_user_id_by_session_id(json_request['session_id'])
+    model.take_out_personal_loan(user_id, amount)
+    return {'message': "Successfully took out personal loan"}
+
+
+def repay_loan(json_request):
+    check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id'])
+    amount = json_request['amount']
+    user_id = model.get_user_id_by_session_id(json_request['session_id'])
+    loan_id = json_request['loan_id']
+    if amount == 'all':
+        amount = model.loan_remaining_amount(loan_id)
+    if amount < 0:
+        raise BadRequest('You can not repay negative amounts.')
+    if model.user_money(user_id) < amount:
+        raise PreconditionFailed('You do not have enough money.')
+    if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id:
+        raise NotFound(f'You do not have a loan with that id.')
+    loan_volume = model.loan_remaining_amount(loan_id)
+    if loan_volume < amount:
+        raise PreconditionFailed(f'You can not repay more than the remaining loan volume of {loan_volume}.')
+    model.repay_loan(loan_id, amount, known_user_id=user_id)
+    return {'message': "Successfully repayed loan"}
+
+
+def before_request(_json_request):
+    # pay interest rates for loans
+    model.pay_loan_interest()