فهرست منبع

Various refactoring, bonds feature

Eren Yilmaz 5 سال پیش
والد
کامیت
314c9256f4
11فایلهای تغییر یافته به همراه293 افزوده شده و 85 حذف شده
  1. 2 2
      README.md
  2. 49 6
      client_controller.py
  3. 51 31
      db_setup/create_triggers.py
  4. 5 5
      db_setup/seeds/__init__.py
  5. 1 0
      db_setup/tables.py
  6. 28 4
      doc/documentation.py
  7. 2 0
      game.py
  8. 102 25
      model.py
  9. 3 0
      routes.py
  10. 36 6
      server_controller.py
  11. 14 6
      test/do_some_requests/__init__.py

+ 2 - 2
README.md

@@ -21,11 +21,11 @@ The server is intended to be run within a python 3.6 environment on the host spe
 
 On the server you can install the required packages using pip:
 ```
-pip3 install bottle requests tabulate
+pip3 install bottle requests tabulate GitPython
 ```
 and start the server using
 ```
-python3 -OO run_server.py
+python3 -O run_server.py
 ```
 
 ### Client

+ 49 - 6
client_controller.py

@@ -1,3 +1,4 @@
+import re
 import sys
 from getpass import getpass
 from inspect import signature
@@ -5,7 +6,7 @@ from inspect import signature
 import connection
 from connection import client_request
 from debug import debug
-from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME, MIN_INTEREST_INTERVAL, CURRENCY_SYMBOL
+from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME, MIN_INTEREST_INTERVAL, CURRENCY_SYMBOL, OWNABLE_NAME_PATTERN
 from routes import client_commands
 from run_client import fake_loading_bar
 from util import my_tabulate, yn_dialog
@@ -167,7 +168,6 @@ def help(command=None):
                                                                    ]))
 
 
-
 def depot():
     fake_loading_bar('Loading data', duration=1.3)
     response = client_request('depot', {"session_id": connection.session_id})
@@ -226,7 +226,7 @@ def _order(is_buy_order, obj_name=None, amount=None, limit=None, stop_loss=None,
         if obj_name == '':
             return
 
-    if stop_loss.strip() not in [None, '1', '0']:
+    if stop_loss is not None and stop_loss.strip() not in ['1', '0']:
         print('Invalid value for flag stop loss (only 0 or 1 allowed).')
         return
     elif stop_loss is not None:
@@ -274,7 +274,7 @@ def _order(is_buy_order, obj_name=None, amount=None, limit=None, stop_loss=None,
     if ioc is None and not stop_loss:
         ioc = yn_dialog('Is this an IOC (immediate-or-cancel) order?')
 
-    if str(ioc).strip() not in ['1', '0', True, False]:
+    if str(ioc).strip() not in ['1', '0', 'True', 'False']:
         print('Invalid value for flag IOC (only 0 or 1 allowed).')
         return
     else:
@@ -305,7 +305,7 @@ def sell(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, io
            limit=limit,
            stop_loss=stop_loss,
            expiry=expiry,
-           ioc=ioc,)
+           ioc=ioc, )
 
 
 def buy(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, ioc=None):
@@ -315,7 +315,7 @@ def buy(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, ioc
            limit=limit,
            stop_loss=stop_loss,
            expiry=expiry,
-           ioc=ioc,)
+           ioc=ioc, )
 
 
 def orders():
@@ -431,6 +431,24 @@ def loans():
             print('Order access failed.')
 
 
+def bonds():
+    fake_loading_bar('Loading Data', duration=1.6)
+    response = client_request('bonds', {"session_id": connection.session_id})
+    success = 'data' in response
+    if success:
+        for row in response['data']:
+            row[1] = f'{row[1] * 100:.4f}%'
+        print(my_tabulate(response['data'],
+                          headers=['Bond', 'Coupon', 'Maturity', 'Issuer',],
+                          floatfmt='.2f',
+                          tablefmt="pipe"))
+    else:
+        if 'error' in response:
+            print('Listing bonds failed with message:', response['error'])
+        else:
+            print('Listing bonds failed.')
+
+
 def repay_loan(loan_id=None, amount=None):
     if loan_id is None:
         loans()
@@ -604,6 +622,31 @@ def old_orders(include_canceled=None, include_executed=None, limit=10):
             print('Order access failed.')
 
 
+def issue_bond(name=None, coupon=None, run_time=None):
+    if name is None:
+        name = input('Name of the bond:')
+    if not re.fullmatch(OWNABLE_NAME_PATTERN, name):
+        print(f'Invalid name: {name}. Name must match {OWNABLE_NAME_PATTERN} and must not be copyright-protected '
+              f'(yes, we have an upload filter for that).')
+
+    if coupon is None:
+        coupon = input('Coupon (e.g. 0.005 for 0.5% p.a.):')
+
+    if run_time is None:
+        run_time = input('Run-time of the bond (minutes):')
+
+    fake_loading_bar('Checking name', duration=3.7)
+    fake_loading_bar('Publishing important information', duration=0.3)
+    response = client_request('issue_bond', {"session_id": connection.session_id,
+                                             "name": name,
+                                             "coupon": coupon,
+                                             "run_time": run_time})
+    if 'error' in response:
+        print('Issuing bond failed with message:', response['error'])
+    elif 'message' in response:
+        print(response['message'])
+
+
 # noinspection PyShadowingBuiltins
 def exit():
     global exiting

+ 51 - 31
db_setup/create_triggers.py

@@ -1,7 +1,7 @@
 import sqlite3
 from typing import List
 
-from game import MINIMUM_ORDER_AMOUNT, CURRENCY_NAME
+from game import MINIMUM_ORDER_AMOUNT, CURRENCY_NAME, BANK_NAME
 
 
 def create_triggers(cursor: sqlite3.Cursor):
@@ -80,39 +80,59 @@ def create_triggers(cursor: sqlite3.Cursor):
                 WHEN NEW.ordered_amount < NEW.executed_amount
                 BEGIN SELECT RAISE(ROLLBACK, 'Can not execute more than ordered.'); END
                 ''')
-    cursor.execute('''
-                CREATE TRIGGER IF NOT EXISTS not_more_ordered_than_available_after_insert
+    cursor.execute(f'''
+                CREATE TRIGGER IF NOT EXISTS not_more_selling_than_available_after_insert
                 AFTER INSERT ON orders
-                WHEN NOT NEW.buy AND 0 >
-                    -- owned_amount
-                    COALESCE (
-                        (SELECT amount
-                         FROM ownership
-                         WHERE ownership.rowid = NEW.ownership_id), 0)
-                    - -- sell_ordered_amount
-                    (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
-                     FROM orders, ownership
-                     WHERE ownership.rowid = orders.ownership_id
-                     AND ownership.rowid = NEW.ownership_id
-                     AND NOT orders.buy) 
-                BEGIN SELECT RAISE(ROLLBACK, 'Can not order more than you own.'); END
+                WHEN (SELECT 
+                    NOT NEW.buy -- is a selling order
+                    AND NOT EXISTS(
+                        SELECT * FROM bonds
+                        WHERE bonds.issuer_id = u.rowid
+                        AND bonds.ownable_id = o.ownable_id) -- not an self-issued bond
+                    AND u.username != '{BANK_NAME}' -- bank may sell any amount
+                    AND 0 >
+                        -- owned_amount
+                        COALESCE (
+                            (SELECT amount
+                             FROM ownership
+                             WHERE ownership.rowid = NEW.ownership_id), 0)
+                        - -- sell_ordered_amount
+                        (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
+                         FROM orders, ownership
+                         WHERE ownership.rowid = orders.ownership_id
+                         AND ownership.rowid = NEW.ownership_id
+                         AND NOT orders.buy)
+                FROM ownership o 
+                JOIN users u ON o.user_id = u.rowid
+                WHERE o.rowid = NEW.ownership_id)
+                BEGIN SELECT RAISE(ROLLBACK, 'Can not sell more than you own.'); END
                 ''')
-    cursor.execute('''
-                CREATE TRIGGER IF NOT EXISTS not_more_ordered_than_available_after_update
+    cursor.execute(f'''
+                CREATE TRIGGER IF NOT EXISTS not_more_selling_than_available_after_update
                 AFTER UPDATE ON orders
-                WHEN NOT NEW.buy AND 0 >
-                    -- owned_amount
-                    COALESCE (
-                        (SELECT amount
-                         FROM ownership
-                         WHERE ownership.rowid = NEW.ownership_id), 0)
-                    - -- sell_ordered_amount
-                    (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
-                     FROM orders, ownership
-                     WHERE ownership.rowid = orders.ownership_id
-                     AND ownership.rowid = NEW.ownership_id
-                     AND NOT orders.buy) 
-                BEGIN SELECT RAISE(ROLLBACK, 'Can not order more than you own.'); END
+                WHEN (SELECT 
+                    NOT NEW.buy -- is a selling order
+                    AND NOT EXISTS(
+                        SELECT * FROM bonds
+                        WHERE bonds.issuer_id = u.rowid
+                        AND bonds.ownable_id = o.ownable_id) -- not an self-issued bond
+                    AND u.username != '{BANK_NAME}' -- bank may sell any amount
+                    AND 0 >
+                        -- owned_amount
+                        COALESCE (
+                            (SELECT amount
+                             FROM ownership
+                             WHERE ownership.rowid = NEW.ownership_id), 0)
+                        - -- sell_ordered_amount
+                        (SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
+                         FROM orders, ownership
+                         WHERE ownership.rowid = orders.ownership_id
+                         AND ownership.rowid = NEW.ownership_id
+                         AND NOT orders.buy)
+                FROM ownership o 
+                JOIN users u ON o.user_id = u.rowid
+                WHERE o.rowid = NEW.ownership_id)
+                BEGIN SELECT RAISE(ROLLBACK, 'Can not sell more than you own.'); END
                 ''')
     cursor.execute('''
                 CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_insert

+ 5 - 5
db_setup/seeds/__init__.py

@@ -1,6 +1,6 @@
 from sqlite3 import Cursor
 
-from game import CURRENCY_NAME, MRO_NAME
+from game import CURRENCY_NAME, MRO_NAME, BANK_NAME
 
 
 def seed(cursor: Cursor):
@@ -18,18 +18,18 @@ def seed(cursor: Cursor):
     cursor.execute('''
                     INSERT OR IGNORE INTO users
                     (username,password)
-                    VALUES ('bank','')
-                    ''')
+                    VALUES (?,'')
+                    ''', (BANK_NAME,))
 
     # bank owns some stuff
     cursor.execute('''
     INSERT OR IGNORE INTO ownership
     (user_id, ownable_id, amount)
-    SELECT (SELECT rowid FROM users WHERE username = 'bank'), 
+    SELECT (SELECT rowid FROM users WHERE username = ?), 
             ownables.rowid, 
             (SELECT COALESCE(SUM(amount),0) FROM ownership WHERE ownable_id = ownables.rowid)
     FROM ownables
-    ''')
+    ''', (BANK_NAME, ))
     cursor.executemany('''
     INSERT INTO global_control_values (value_name, value)
     WITH new_value AS (SELECT ? AS name, ? AS value)

+ 1 - 0
db_setup/tables.py

@@ -126,6 +126,7 @@ def tables(cursor):
                     issuer_id INTEGER NOT NULL REFERENCES users(rowid),
                     ownable_id INTEGER UNIQUE NOT NULL REFERENCES ownables(rowid),
                     last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
+                    maturity_dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     coupon CURRENCY NOT NULL -- fancy word for interest rate
                 )
                 ''')

+ 28 - 4
doc/documentation.py

@@ -8,6 +8,15 @@
 activate_key_required_attributes = ['key', 'session_id']
 activate_key_possible_attributes = ['key', 'session_id']
 
+before_request_required_attributes = []
+before_request_possible_attributes = []
+
+bonds_required_attributes = []
+bonds_possible_attributes = []
+
+buy_banking_license_required_attributes = ['session_id']
+buy_banking_license_possible_attributes = ['session_id']
+
 cancel_order_required_attributes = ['order_id', 'session_id']
 cancel_order_possible_attributes = ['order_id', 'session_id']
 
@@ -20,14 +29,23 @@ depot_possible_attributes = ['session_id']
 gift_required_attributes = ['amount', 'object_name', 'session_id', 'username']
 gift_possible_attributes = ['amount', 'object_name', 'session_id', 'username']
 
+global_variables_required_attributes = []
+global_variables_possible_attributes = []
+
+issue_bond_required_attributes = ['coupon', 'name', 'run_time', 'session_id']
+issue_bond_possible_attributes = ['coupon', 'name', 'run_time', 'session_id']
+
 leaderboard_required_attributes = []
 leaderboard_possible_attributes = []
 
+loans_required_attributes = ['session_id']
+loans_possible_attributes = ['session_id']
+
 login_required_attributes = ['password', 'username']
 login_possible_attributes = ['password', 'username']
 
-missing_attributes_required_attributes = []
-missing_attributes_possible_attributes = []
+logout_required_attributes = ['session_id']
+logout_possible_attributes = ['session_id']
 
 news_required_attributes = []
 news_possible_attributes = []
@@ -36,7 +54,7 @@ old_orders_required_attributes = ['include_canceled', 'include_executed', 'limit
 old_orders_possible_attributes = ['include_canceled', 'include_executed', 'limit', 'session_id']
 
 order_required_attributes = ['amount', 'buy', 'ownable', 'session_id', 'time_until_expiration']
-order_possible_attributes = ['amount', 'buy', 'ownable', 'session_id', 'time_until_expiration']
+order_possible_attributes = ['amount', 'buy', 'ioc', 'limit', 'ownable', 'session_id', 'stop_loss', 'time_until_expiration']
 
 orders_required_attributes = ['session_id']
 orders_possible_attributes = ['session_id']
@@ -45,7 +63,13 @@ orders_on_required_attributes = ['ownable', 'session_id']
 orders_on_possible_attributes = ['ownable', 'session_id']
 
 register_required_attributes = ['password', 'username']
-register_possible_attributes = ['password', 'username']
+register_possible_attributes = ['game_key', 'password', 'username']
+
+repay_loan_required_attributes = ['amount', 'loan_id', 'session_id']
+repay_loan_possible_attributes = ['amount', 'loan_id', 'session_id']
+
+take_out_personal_loan_required_attributes = ['amount', 'session_id']
+take_out_personal_loan_possible_attributes = ['amount', 'session_id']
 
 tradables_required_attributes = []
 tradables_possible_attributes = []

+ 2 - 0
game.py

@@ -12,3 +12,5 @@ MIN_INTEREST_INTERVAL = 60  # seconds
 ROOT_URL = "/orderer.zip"
 
 logger = DBLog()
+OWNABLE_NAME_PATTERN = r'[A-Z-a-z0-9]{1,6}'
+BANK_NAME = 'bank'

+ 102 - 25
model.py

@@ -13,7 +13,7 @@ from typing import Optional, Dict
 from passlib.handlers.sha2_crypt import sha256_crypt
 
 import db_setup
-from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL, MRO_NAME
+from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL, MRO_NAME, BANK_NAME
 from util import random_chars
 
 DBName = str
@@ -181,7 +181,7 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
         raise AssertionError('Can not send negative amount')
 
     bank_id_ = bank_id()
-    if from_user_id != bank_id_:
+    if from_user_id != bank_id_ and not is_bond_of_user(ownable_id, from_user_id):
         execute('''
                     UPDATE ownership
                     SET amount = amount - ?
@@ -191,7 +191,7 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
 
     own(to_user_id, ownable_name_by_id(ownable_id))
 
-    if to_user_id != bank_id_ or ownable_id != currency_id():
+    if to_user_id != bank_id_ and not is_bond_of_user(ownable_id, to_user_id):
         execute('''
                     UPDATE ownership
                     SET amount = amount + ?
@@ -334,8 +334,8 @@ def bank_id():
     execute('''
         SELECT users.rowid
         FROM users
-        WHERE username = 'bank'
-        ''')
+        WHERE username = ?
+        ''', (BANK_NAME,))
 
     return current_cursor.fetchone()[0]
 
@@ -395,6 +395,22 @@ def get_user_loans(user_id):
     return current_cursor.fetchall()
 
 
+def bonds():
+    execute('''
+        SELECT 
+            name,
+            coupon,
+            datetime(maturity_dt, 'unixepoch', 'localtime'),
+            username           
+        FROM bonds
+        JOIN ownables o on bonds.ownable_id = o.rowid
+        JOIN users issuer on bonds.issuer_id = issuer.rowid
+        ORDER BY coupon * (maturity_dt - ?) DESC
+        ''', (current_db_timestamp(),))
+
+    return current_cursor.fetchall()
+
+
 def get_ownable_orders(user_id, ownable_id):
     execute('''
         SELECT 
@@ -925,34 +941,45 @@ def user_has_order_with_id(session_id, order_id):
 
 
 def leaderboard():
-    execute('''
+    score_expression = '''
+    SELECT (
+        SELECT COALESCE(SUM(
+            CASE -- sum score for each of the users ownables
+            WHEN ownership.ownable_id = ? THEN ownership.amount
+            ELSE ownership.amount * (SELECT price 
+                                     FROM transactions
+                                     WHERE ownable_id = ownership.ownable_id 
+                                     ORDER BY rowid DESC -- equivalent to ordering by dt
+                                     LIMIT 1)
+            END
+        ), 0)
+        FROM ownership
+        WHERE ownership.user_id = users.rowid)
+        -
+        ( SELECT COALESCE(SUM(
+            amount
+        ), 0)
+        FROM loans
+        WHERE loans.user_id = users.rowid)
+    '''
+    execute(f'''
         SELECT * 
         FROM ( -- one score for each user
             SELECT 
                 username, 
-                SUM(CASE -- sum score for each of the users ownables
-                    WHEN ownership.ownable_id = ? THEN ownership.amount
-                    ELSE ownership.amount * (SELECT price 
-                                             FROM transactions
-                                             WHERE ownable_id = ownership.ownable_id 
-                                             ORDER BY rowid DESC -- equivalent to ordering by dt
-                                             LIMIT 1)
-                    END
-                ) score
-            FROM users, ownership
-            WHERE ownership.user_id = users.rowid
-            AND users.username != 'bank'
-            GROUP BY users.rowid
+                ({score_expression}) AS score
+                FROM users
+            WHERE users.username != ?
         ) AS scores
         ORDER BY score DESC
         LIMIT 50
-        ''', (currency_id(),))
+        ''', (currency_id(), BANK_NAME))
 
     return current_cursor.fetchall()
 
 
 def user_wealth(user_id):
-    execute('''
+    score_expression = '''
     SELECT (
         SELECT COALESCE(SUM(
             CASE -- sum score for each of the users ownables
@@ -972,6 +999,9 @@ def user_wealth(user_id):
         ), 0)
         FROM loans
         WHERE loans.user_id = ?)
+    '''
+    execute(f'''
+    SELECT ({score_expression}) AS score
         ''', (currency_id(), user_id, user_id,))
 
     return current_cursor.fetchone()[0]
@@ -1241,6 +1271,53 @@ def assign_banking_licence(user_id):
         ''', (user_id,))
 
 
+def pay_bond_interest():
+    current_dt = execute("SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)").fetchone()[0]
+    sec_per_year = 3600 * 24 * 365
+    interests = execute('''
+    SELECT 
+        SUM(amount * (POWER(1 + coupon, 
+                            (MIN(CAST(? AS FLOAT), maturity_dt) - last_interest_pay_dt) / ?) - 1)
+            ) AS interest_since_last_pay,
+        o.user_id AS to_user_id,
+        bonds.issuer_id AS from_user_id
+    FROM bonds
+    JOIN ownership o on bonds.ownable_id = o.ownable_id
+    WHERE ? - last_interest_pay_dt > ? OR ? > maturity_dt -- every interval or when the bond expired
+    AND amount != 0
+    GROUP BY o.user_id, bonds.issuer_id
+    ''', (current_dt, sec_per_year, current_dt, MIN_INTEREST_INTERVAL, current_dt)).fetchall()
+
+    matured_bonds = execute('''
+    SELECT 
+        amount,
+        o.user_id AS to_user_id,
+        bonds.issuer_id AS from_user_id
+    FROM bonds
+    JOIN ownership o on bonds.ownable_id = o.ownable_id
+    WHERE ? > maturity_dt
+    ''', (current_dt,)).fetchall()
+
+    # transfer the interest money
+    for amount, to_user_id, from_user_id in interests:
+        send_ownable(from_user_id, to_user_id, currency_id(), amount)
+
+    # pay back matured bonds
+    for amount, to_user_id, from_user_id in matured_bonds:
+        send_ownable(from_user_id, to_user_id, currency_id(), amount)
+
+    execute('''
+    UPDATE bonds 
+    SET last_interest_pay_dt = ?
+    WHERE ? - last_interest_pay_dt > ?''', (current_dt, current_dt, MIN_INTEREST_INTERVAL,))
+
+    # delete matured bonds
+    execute('''
+    DELETE FROM bonds 
+    WHERE ? > maturity_dt
+    ''', (current_dt,))
+
+
 def pay_loan_interest():
     current_dt = execute("SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)").fetchone()[0]
     sec_per_year = 3600 * 24 * 365
@@ -1328,12 +1405,12 @@ def main_refinancing_operations():
     ...  # TODO
 
 
-def issue_bond(user_id, ownable_name, coupon):
+def issue_bond(user_id, ownable_name, coupon, maturity_dt):
     execute('''
     INSERT INTO ownables(name)
     VALUES (?)
     ''', (ownable_name,))
     execute('''
-    INSERT INTO bonds(issuer_id, ownable_id, coupon) 
-    VALUES (?, (SELECT MAX(rowid) FROM ownables), ?)
-    ''', (user_id, coupon))
+    INSERT INTO bonds(issuer_id, ownable_id, coupon, maturity_dt) 
+    VALUES (?, (SELECT MAX(rowid) FROM ownables), ?, ?)
+    ''', (user_id, coupon, maturity_dt))

+ 3 - 0
routes.py

@@ -21,6 +21,7 @@ valid_post_routes = {
     'repay_loan',
     'issue_bond',
     'loans',
+    'bonds',
 }
 
 push_message_types = set()
@@ -39,6 +40,7 @@ client_commands = ['help',
                    'login',
                    'register',
                    'change_pw',
+                   'bonds',
                    'news',
                    'tradables',
                    'take_out_personal_loan',
@@ -53,6 +55,7 @@ client_commands = ['help',
                    'buy',
                    'sell',
                    'cancel_order',
+                   'issue_bond',
                    'gift',
                    'leaderboard',
                    'activate_key',

+ 36 - 6
server_controller.py

@@ -1,3 +1,4 @@
+import re
 import uuid
 from datetime import timedelta
 from math import ceil, floor
@@ -6,6 +7,7 @@ from passlib.hash import sha256_crypt
 
 import model
 from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound
+from game import OWNABLE_NAME_PATTERN, BANK_NAME
 
 
 def login(json_request):
@@ -81,7 +83,10 @@ def order(json_request):
 
     amount = json_request['amount']
     try:
-        amount = int(amount)
+        amount = float(amount)  # so that something like 5e6 also works but only integers
+        if amount != round(amount):
+            raise ValueError
+        amount = round(amount)
     except ValueError:
         return BadRequest('Invalid amount.')
     if amount < 0:
@@ -153,7 +158,7 @@ def gift(json_request):
     check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username'])
     if not model.ownable_name_exists(json_request['object_name']):
         return BadRequest('This kind of object can not be given away.')
-    if json_request['username'] == 'bank' or not model.user_exists(json_request['username']):
+    if json_request['username'] == BANK_NAME or not model.user_exists(json_request['username']):
         return BadRequest('There is no user with this name.')
     try:
         amount = float(json_request['amount'])
@@ -190,6 +195,11 @@ def loans(json_request):
     return {'data': data}
 
 
+def bonds(_json_request):
+    data = model.bonds()
+    return {'data': data}
+
+
 def orders_on(json_request):
     check_missing_attributes(json_request, ['session_id', 'ownable'])
     if not model.ownable_name_exists(json_request['ownable']):
@@ -227,6 +237,12 @@ def change_password(json_request):
     return {'message': "Successfully changed password"}
 
 
+def logout(json_request):
+    check_missing_attributes(json_request, ['session_id'])
+    model.sign_out_user(json_request['session_id'])
+    return {'message': "Successfully logged out"}
+
+
 def buy_banking_license(json_request):
     check_missing_attributes(json_request, ['session_id'])
     user_id = model.get_user_id_by_session_id(json_request['session_id'])
@@ -275,11 +291,25 @@ def take_out_personal_loan(json_request):
 
 
 def issue_bond(json_request):
-    check_missing_attributes(json_request, ['session_id', 'name', 'coupon', ])
+    check_missing_attributes(json_request, ['session_id', 'name', 'coupon', 'run_time'])
     user_id = model.get_user_id_by_session_id(json_request['session_id'])
     coupon = json_request['coupon']
+    try:
+        coupon = float(coupon)
+    except ValueError:
+        raise BadRequest('Coupon must be a number.')
     ownable_name = json_request['name']
-    model.issue_bond(user_id, ownable_name, coupon)
+    if not re.fullmatch(OWNABLE_NAME_PATTERN, ownable_name):
+        raise BadRequest('Invalid name.')
+    run_time = json_request['run_time']
+    try:
+        run_time = int(run_time)
+    except ValueError:
+        raise BadRequest('Run-time must be a positive integer number.')
+    if run_time < 0:
+        raise BadRequest('Run-time must be a positive integer number.')
+    maturity_dt = model.current_db_timestamp() + 60 * run_time
+    model.issue_bond(user_id, ownable_name, coupon, maturity_dt)
     return {'message': "Successfully issued bond"}
 
 
@@ -307,5 +337,5 @@ def before_request(_json_request):
     # pay interest rates for loans
     model.pay_loan_interest()
 
-    # main refinancing operation
-    model.main_refinancing_operations()
+    # pay interest rates for bonds
+    model.pay_bond_interest()

+ 14 - 6
test/do_some_requests/__init__.py

@@ -120,6 +120,8 @@ def run_tests():
     usernames = [f'user{datetime.now().timestamp()}',
                  f'user{datetime.now().timestamp()}+1']
 
+    banks = usernames[:1]
+
     session_ids = {}
 
     message = {}
@@ -168,11 +170,7 @@ def run_tests():
             route = 'change_password'
             default_request_method(route, message)
 
-            message = {'session_id': session_ids[username]}
-            route = 'logout'
-            default_request_method(route, message)
-
-            message = {'username': username, 'password': DEFAULT_PW}
+            message = {'username': username, 'password': password}
             route = 'login'
             session_ids[username] = default_request_method(route, message)['session_id']
 
@@ -182,12 +180,22 @@ def run_tests():
             data = default_request_method(route, message)['data']
             assert len(data) <= limit
 
-    for session_id in session_ids:
+    for username in banks:
+        message = {'session_id': session_ids[username], 'amount': 5.5e6}
+        route = 'take_out_personal_loan'
+        default_request_method(route, message)
+
+        message = {'session_id': session_ids[username]}
+        route = 'buy_banking_license'
+        default_request_method(route, message)
+
+    for session_id in session_ids.values():
         message = {'session_id': session_id}
         route = 'logout'
         default_request_method(route, message)
 
 
+
 def main():
     global default_request_method
     for m in [