From 807cf4221d6343f884628daa4a1288a3c5212c02 Mon Sep 17 00:00:00 2001 From: ryouthere Date: Mon, 8 Aug 2022 09:18:54 +0300 Subject: [PATCH] Implemented all features. --- .idea/APIVendingMachine.iml | 2 +- .idea/misc.xml | 5 +- Dockerfile | 14 ++ README.md | 18 +- extensions.py | 3 + main.py | 453 +++++++++++++++++++++++++++++++++++- models.py | 116 +++++++-- requirements.txt | Bin 0 -> 762 bytes test_fill_database.py | 64 +++++ utils.py | 93 +------- 10 files changed, 638 insertions(+), 130 deletions(-) create mode 100644 Dockerfile create mode 100644 extensions.py create mode 100644 requirements.txt create mode 100644 test_fill_database.py diff --git a/.idea/APIVendingMachine.iml b/.idea/APIVendingMachine.iml index d0876a7..d9de42d 100644 --- a/.idea/APIVendingMachine.iml +++ b/.idea/APIVendingMachine.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d56657a..fb39efe 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ - + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5513928 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10 + +LABEL Author="Mike" + +RUN mkdir /app +WORKDIR /app + +COPY / ./ + +RUN pip install -r requirements.txt + +EXPOSE 4231 + +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index 9a96291..e7a2c7f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # APIVendingMachine -Design an API for a vending machine, allowing users with a “seller” role to add, update or remove products, while users with a “buyer” role can deposit coins into the machine and make purchases. Your vending machine should only accept 5, 10, 20, 50 and 100 cent coins +API for a vending machine, allowing users with a “seller” role to add, update or remove products, while users with a “buyer” role can deposit coins into the machine and make purchases. Your vending machine should only accept 5, 10, 20, 50 and 100 cent coins -**Tasks** +**Key points** - REST API should be implemented consuming and producing “application/json” - Implement product model with amountAvailable, cost (should be in multiples of 5), productName and sellerId fields @@ -15,19 +15,5 @@ Design an API for a vending machine, allowing users with a “seller” role to - Implement /buy endpoint (accepts productId, amount of products) so users with a “buyer” role can buy a product (shouldn't be able to buy multiple different products at the same time) with the money they’ve deposited. API should return total they’ve spent, the product they’ve purchased and their change if there’s any (in an array of 5, 10, 20, 50 and 100 cent coins) - Implement /reset endpoint so users with a “buyer” role can reset their deposit back to 0 - Take time to think about possible edge cases and access issues that should be solved - -**Evaluation criteria:** - -- Language/Framework of choice best practices -- Edge cases covered -- Write tests for /deposit, /buy and one CRUD endpoint of your choice -- Code readability and optimization - -**Bonus:** - - If somebody is already logged in with the same credentials, the user should be given a message "There is already an active session using your account". In this case the user should be able to terminate all the active sessions on their account via an endpoint i.e. /logout/all -- Attention to security - -## Deliverables -A Github repository with public access. Please have the solution running and a Postman / Swagger collection ready on your computer so the domain expert can tell you which tests to run on the API. \ No newline at end of file diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/extensions.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/main.py b/main.py index 19a207f..6832104 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,17 @@ -# use request.get_json() to get flask to use application/json, probably with force=True flag -# creates Flask object -from flask import Flask -from dotenv import load_dotenv import os +from copy import copy +from datetime import datetime, timedelta +from functools import wraps + +import jwt +from dotenv import load_dotenv +from flask import Flask, jsonify, request, abort, make_response +from werkzeug.security import check_password_hash + +from extensions import db +from models import User, AlchemyEncoder, Role, Product, LoggedInUser, clean_invalid_tokens, expire_all_user_tokens, \ + token_manually_expired +from utils import calculate_change load_dotenv() @@ -13,15 +22,435 @@ POSTGRES_USER = os.getenv('POSTGRES_USER') POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD') -app = Flask(__name__) -# configuration -# NEVER HARDCODE YOUR CONFIGURATION IN YOUR CODE -# INSTEAD CREATE A .env FILE AND STORE IN IT -app.config['SECRET_KEY'] = 'your secret key' -# database name -app.config['SQLALCHEMY_DATABASE_URI'] = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True +def register_extensions(app): + db.init_app(app) + + +def create_app(): + app = Flask(__name__) + app.json_encoder = AlchemyEncoder + + app.config['SECRET_KEY'] = 'big secret' + # database name + app.config[ + 'SQLALCHEMY_DATABASE_URI'] = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True + + register_extensions(app) + + return app + + +app = create_app() + + +def token_required(f): + """ + JWT token-based auth wrapper + """ + + @wraps(f) + def decorated(*args, **kwargs): + token = None + # jwt is passed in the request header + if 'Authorization' in request.headers: + token = request.headers.get('Authorization') + # return 401 if token is not passed + if not token: + return jsonify({'message': 'Token is missing !!'}), 401 + + try: + # Check if token is valid + token = token.replace("Bearer ", '') + data = jwt.decode( + jwt=token, + key=app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + # Check if token hasn't been manually expired + if token_manually_expired(data['id'], token): + return jsonify({'message': 'You have been logged out!'}), 401 + + current_user = User.query \ + .filter_by(id=data['id']) \ + .first() + except Exception as e: + return jsonify({ + 'message': 'Token is invalid !!' + }), 401 + # returns the current logged in users contex to the routes + return f(current_user, *args, **kwargs) + + return decorated + + +def user_has_permission(f): + """ + Wrapper to determine if user can access or edit + certain resources. + """ + + @wraps(f) + def decorated(current_user, id, *args, **kwargs): + if current_user.id == int(id) or 'admin' in [x.role_name for x in current_user.roles]: + return f(current_user, id, *args, **kwargs) + return abort(403, "You don't have the necessary permission to access this data") + + return decorated + + +def user_is_seller(f): + """ + Wrapper to determine if the user is a seller + """ + + @wraps(f) + def decorated(current_user, *args, **kwargs): + user_roles = [x.role_name for x in current_user.roles] + if 'seller' in user_roles or \ + 'admin' in user_roles: + return f(current_user, *args, **kwargs) + return abort(403, "You don't have the necessary permission to access this data") + + return decorated + + +def user_is_buyer(f): + """ + Wrapper to determine if the user is a buyer + """ + + @wraps(f) + def decorated(current_user, *args, **kwargs): + user_roles = [x.role_name for x in current_user.roles] + if 'buyer' in user_roles or \ + 'admin' in user_roles: + return f(current_user, *args, **kwargs) + return abort(403, "You don't have the necessary permission to access this endpoint") + + return decorated + + +@app.route('/login', methods=['POST']) +def login(): + """ + Route for logging in and obtaining the JWT token + """ + + if not request.json: + abort(400) + auth = request.json + if not auth.get('username') or not auth.get('password'): + # returns 401 if any username or / and password is missing + return make_response( + 'Could not verify', + 401, + {'WWW-Authenticate': 'Basic realm ="Login required"'} + ) + + user = User.query \ + .filter_by(username=auth.get('username')) \ + .first_or_404() + + if check_password_hash(user.password, auth.get('password')): + # generates the JWT Token + pkg = { + 'token': jwt.encode({ + 'id': user.id, + 'exp': datetime.utcnow() + timedelta(minutes=30) + }, app.config['SECRET_KEY']) + } + + # First clean database of invalid tokens + valid_sessions = clean_invalid_tokens(user, app.config['SECRET_KEY']) + + # Prompt user if there is someone else logged in with these credentials: + + if valid_sessions: + pkg['warning'] = f"User already logged in {len(valid_sessions)} other sessions, " \ + f"use /logout/all to exit all sessions." + + # Add current jwt token to database + new_login = LoggedInUser( + user_id=user.id, + token=pkg['token'], + token_status='valid', + ) + db.session.add(new_login) + db.session.commit() + + return make_response(jsonify(pkg), 201) + # returns 403 if password is wrong + return make_response( + 'Could not verify', + 403, + {'WWW-Authenticate': 'Basic realm ="Wrong Password"'} + ) + + +@app.route('/logout/all', methods=["GET"]) +@token_required +def logout(current_user): + # Expires all tokens. + expire_all_user_tokens(current_user) + return jsonify({'result': True}) + + +@app.route('/user/', methods=['POST']) +def create_user(): + """ + Route to create a user, public access. + """ + + if not request.json: + abort(400) + # Username conflict (it should be treated in frontEnd) + if User.query.filter_by(username=request.json.get('username')).first(): + abort(409, 'Username already exists, please pick another username') + + # Password requirements, simple example (it should be treated in frontEnd) + if len(request.json.get('password')) < 4: + abort(422, 'Password does not meet minimum requirements') + + new_user = User(username=request.json.get('username'), + password=request.json.get('password'), + deposit=request.json.get('deposit') + ) + for role_name in request.json.get('roles'): + new_user.roles.append(Role.query.filter_by(role_name=role_name).first()) + db.session.add(new_user) + db.session.commit() + return jsonify(new_user.to_dict(rules=('-_password',))) + + +# USER API ENDPOINTS + +@app.route('/user/', methods=["GET"]) +@token_required +@user_has_permission +def get_user(current_user, id): + """ + Route to get user data, will be restricted as such: + 1. Logged in users can acccess their data. + 2. Admins can access other users's data. + """ + user = User.query.filter_by(id=id).first_or_404() + return user.to_dict(rules=('-_password',)) + + +@app.route("/user/", methods=["DELETE"]) +@token_required +@user_has_permission +def delete_user(current_user, id): + """ + Route to delete user data, will be restricted as such: + 1. Logged in users can delete their account. + 2. Admins can access other users's data. + """ + user = User.query.filter_by(id=id).first_or_404() + db.session.delete(user) + db.session.commit() + return jsonify({'result': True}) + + +@app.route("/user/", methods=["PUT"]) +@token_required +@user_has_permission +def update_user(current_user, id): + """ + Route to update user data, will be restricted as such: + 1. Logged in users can delete their account. + 2. Admins can access other users's data. + """ + + if not request.json: + abort(400) + user = User.query.filter_by(id=id).first_or_404() + if request.json.get('username'): + user.username = request.json.get('username') + if request.json.get('password'): + user.password = request.json.get('password') + if request.json.get('deposit'): + user.deposit = request.json.get('deposit') + # Here it can probably implemented in a number of ways, + # we're assuming that on update there will be mentioned all + # the new roles of the user. + if request.json.get('roles'): + user.roles = [] + for role_name in request.json.get('roles'): + user.roles.append(Role.query.filter_by(role_name=role_name).first()) + db.session.add(user) + db.session.commit() + return jsonify({'result': True}) + + +# PRODUCTS API ENDPOINTS + +@app.route('/product/', methods=['POST']) +@token_required +@user_is_seller +def create_product(current_user): + """ + Route to create a product. + Only authenticated users with seller role + """ + + if not request.json: + abort(400) + + new_product = Product(amount_available=request.json.get('amount_available'), + cost=request.json.get('cost'), + product_name=request.json.get('product_name'), + seller=current_user.id, + ) + db.session.add(new_product) + db.session.commit() + return jsonify(new_product.to_dict()) + + +@app.route("/product/", methods=["DELETE"]) +@token_required +@user_is_seller +def delete_product(current_user, id): + """ + Route to delete product, will be restricted as such: + 1. Logged in sellers. + 2. Admins can access other users's data. + """ + product = Product.query.filter_by(id=id).first_or_404() + if product.seller == current_user.id or 'admin' in [x.role_name for x in current_user.roles]: + db.session.delete(product) + db.session.commit() + return jsonify({'result': True}) + else: + abort(403, "Not your product, bro.") + + +@app.route("/product/", methods=["PUT"]) +@token_required +@user_is_seller +def update_product(current_user, id): + """ + Route to update product, will be restricted as such: + 1. Logged in sellers. + 2. Admins can access other users's data. + """ + + if not request.json: + abort(400) + + product = Product.query.filter_by(id=id).first_or_404() + if product.seller == current_user.id or 'admin' in [x.role_name for x in current_user.roles]: + if request.json.get('amount_available'): + product.amount_available = request.json.get('amount_available') + if request.json.get('cost'): + product.cost = request.json.get('cost') + if request.json.get('product_name'): + product.product_name = request.json.get('product_name') + db.session.add(product) + db.session.commit() + return jsonify({'result': True}) + else: + abort(403, "Not your product, bro.") + + +@app.route('/product/', methods=["GET"]) +@token_required +def get_product(current_user, id): + """ + Route to get product info, will be restricted as such: + 1. Logged in users. + """ + product = Product.query.filter_by(id=id).first_or_404() + return product.to_dict() + + +@app.route('/deposit/', methods=["POST"]) +@token_required +@user_is_buyer +def deposit(current_user): + """ + Route to deposit, will be restricted as such: + 1. Logged in buyers. + """ + if not request.json: + abort(400) + amount = request.json.get('amount') + if not amount or amount not in [5, 10, 20, 50, 100]: + abort(409, "Please provide a request with an `amount` key. Value must" + "be 5,10,20,50 or 100.") + current_user.deposit += amount + db.session.add(current_user) + db.session.commit() + return jsonify({'result': f"Inserted {amount} into wallet."}) + + +@app.route('/buy/', methods=["POST"]) +@token_required +@user_is_buyer +def buy(current_user): + """ + Route to buy products. Rules: + 1. Logged in buyers. + 2. Can buy only one product at a time, + 3. Returns total spent, product purchased and change + 4. JSON arguments needed: product_id, amount + """ + + if not request.json: + abort(400) + product_id = request.json.get('product_id') + amount = request.json.get('amount') + if not amount or not product_id: + abort(409, "Please provide a request with an `amount` key.") + + product = Product.query.filter_by(id=product_id).first_or_404() + necessary_funds = product.cost * int(amount) + + # Check if user has necessary funds + if not current_user.deposit >= necessary_funds: + abort(409, f"Not enough cash. Your deposit stands at {current_user.deposit}" + f" while necessary_funds stands at {necessary_funds}") + + # Check if there are enough products + if not product.amount_available >= int(amount): + abort(409, f"Insufficient product amount, maximum available: {product.amount_available}") + + change = current_user.deposit - necessary_funds + current_user.deposit = 0 + product.amount_available -= int(amount) + db.session.add(current_user) + db.session.add(product) + db.session.commit() + return jsonify({ + 'result': "Transaction complete", + 'total_spent': necessary_funds, + 'product': product.product_name, + 'change': calculate_change(change), + }) + + +@app.route('/reset/', methods=["GET"]) +@token_required +@user_is_buyer +def reset(current_user): + """ + Route to reset deposit, should return change. + 1. Logged in buyers can access + """ + change = copy(current_user.deposit) + if change != 0: + current_user.deposit = 0 + db.session.add(current_user) + db.session.commit() + return jsonify({ + 'result': "Reset complete, here's your change", + 'change': calculate_change(change), + }) +if __name__ == '__main__': + db.init_app(app) + app.run(host='0.0.0.0', port=4231) diff --git a/models.py b/models.py index 9c65430..94419e7 100644 --- a/models.py +++ b/models.py @@ -1,29 +1,52 @@ # creates SQLALCHEMY object -from flask_sqlalchemy import SQLAlchemy +import jwt +from flask import json from flask_bcrypt import generate_password_hash -from werkzeug.security import generate_password_hash +from jwt import ExpiredSignatureError +from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy_serializer import SerializerMixin +from werkzeug.security import generate_password_hash -from main import app - -db = SQLAlchemy(app) - +from extensions import db +# Relationship table between users and roles user_roles = db.Table('user_roles', db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True) ) -class User(db.Model): +class AlchemyEncoder(json.JSONEncoder): + """ + JSON encoder + """ + + def default(self, o): + if isinstance(o.__class__, DeclarativeMeta): + data = {} + fields = o.__json__() if hasattr(o, '__json__') else dir(o) + for field in [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]: + value = o.__getattribute__(field) + try: + json.dumps(value) + data[field] = value + except TypeError: + data[field] = None + return data + return json.JSONEncoder.default(self, o) + + +class User(db.Model, SerializerMixin): + serialize_rules = ('-products.user',) + id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100)) _password = db.Column(db.String(128)) - deposit = db.Column(db.Integer) + deposit = db.Column(db.Integer, default=0) - roles = db.relationship('Role', secondary=user_roles, - backref='role') - products = db.relationship("Product", backref='product') + roles = db.relationship('Role', secondary=user_roles) + products = db.relationship("Product") def __repr__(self): return '' % self.username @@ -37,22 +60,77 @@ def password(self, plaintext): self._password = generate_password_hash(plaintext) -class Product(db.Model): +class Product(db.Model, SerializerMixin): + serialize_rules = ('-products.user',) + # serialize_rules = ('-products.user', '-seller') + id = db.Column(db.Integer, primary_key=True) - amountAvailable = db.Column(db.Integer) + amount_available = db.Column(db.Integer) cost = db.Column(db.Integer, db.CheckConstraint("cost%5=0")) - productName = db.Column(db.String(100)) + product_name = db.Column(db.String(100)) - sellerID = db.Column(db.Integer, db.ForeignKey('user.id')) + seller = db.Column(db.Integer, db.ForeignKey('user.id')) def __repr__(self): - return '' % self.productName + return '' % self.product_name + +class Role(db.Model, SerializerMixin): + serialize_rules = ('-roles.user',) -class Role(db.Model): id = db.Column(db.Integer, primary_key=True) - roleName = db.Column(db.String(100)) + role_name = db.Column(db.String(100)) def __repr__(self): - return '' % self.roleName + return '' % self.role_name + +class LoggedInUser(db.Model, SerializerMixin): + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + token = db.Column(db.String(256)) + token_status = db.Column(db.String(64)) + + +def clean_invalid_tokens(user, secret_key): + valid_tokens = [] + other_sessions = LoggedInUser.query.filter_by( + user_id=user.id, + token_status='valid' + ).all() + for session in other_sessions: + try: + jwt.decode( + jwt=session.token, + key=secret_key, + algorithms='HS256' + ) + except ExpiredSignatureError: + session.token_status = 'expired' # this way we also keep login history, can be simply deleted otherwise + db.session.add(session) + continue + valid_tokens.append(session) + db.session.commit() + return valid_tokens + + +def expire_all_user_tokens(user): + db.session.query(LoggedInUser).filter_by(user_id=user.id).update({'token_status': 'expired'}) + db.session.commit() + + +def token_manually_expired(user_id, token): + return LoggedInUser.query.filter_by( + user_id=user_id, + token=token, + token_status='expired' + ).first() + + +def get_role(role_name): + return Role.query.filter(Role.role_name == role_name).first() + + +def get_user(username): + return User.query.filter(User.username == username).first() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..6d9990bac10eb5080c0b1b9dc8c5c0bb1d195dde GIT binary patch literal 762 zcmZ`%%TB{U5c3&{Pf0~mD&W8Y4jd|wKt<}ggtUZ~Y}6#6^6`K@vuVpq(MHLP*JFG4 z^WyQEQl&*qLMO+aR^AwbtrkAWXc%-_DE|gwC$BJBtOK3-qiLLNVWEMT{ zcW+o<(_hDvnecl~u6+)7P?LE>#et|rFHUrn8Ga)V@z?s<=2^b zmfGDik3gjfT?PKh{i|V>WM+9f`o9e2S)4=aQbvE_5;{>@C6($Hy>GSlyWR2B>Pzc) Y^t^M^pX!_C+TYxUigoKeJy$FZU#u%{aR2}S literal 0 HcmV?d00001 diff --git a/test_fill_database.py b/test_fill_database.py new file mode 100644 index 0000000..26c8f3d --- /dev/null +++ b/test_fill_database.py @@ -0,0 +1,64 @@ +import sqlalchemy +from models import User + +from extensions import db +from main import create_app +from utils import logger + + +def create_tables(db): + db.drop_all() + db.create_all() + + +def fill_db_test(db): + from models import User, Product, Role + + user1 = User(username='user1', password='pass1') + user2 = User(username='user2', password='pass2') + user3 = User(username='user3', password='pass3') + + user1.products.append(Product(product_name='p1', amount_available=10, cost=5, seller=user1)) + user1.products.append(Product(product_name='p4', amount_available=100, cost=20, seller=user1)) + user3.products.append(Product(product_name='p2', amount_available=10, cost=10, seller=user2)) + user3.products.append(Product(product_name='p3', amount_available=10, cost=15, seller=user3)) + + role1 = Role(role_name='buyer') + role2 = Role(role_name='seller') + role3 = Role(role_name='admin') + + user1.roles.append(role1) + user1.roles.append(role2) + user1.roles.append(role3) + + user2.roles.append(role1) + user3.roles.append(role2) + + db.session.add_all([user1, user2, user3]) + + db.session.commit() + + +def test_cost_constraint(): + """ + Check if the cost of the product can only be entered in multiples + of 5. Will throw an IntegrityError. + """ + + from models import db, Product + + user = db.session.query(User).all()[0] + + user.products.append(Product(productName='p1', amountAvailable=10, cost=6, sellerID=user)) + db.session.add_all([user]) + try: + db.session.commit() + except sqlalchemy.exc.IntegrityError: + logger.info("Test passed, cost can only be multiples of 5.") + + +app = create_app() +db.init_app(app) +with app.app_context(): + create_tables(db) + fill_db_test(db) diff --git a/utils.py b/utils.py index 96b4055..e8ecd03 100644 --- a/utils.py +++ b/utils.py @@ -1,11 +1,4 @@ -# decorator for verifying the JWT import logging -from functools import wraps -import jwt -import sqlalchemy.exc -from flask import jsonify -from models import User -from main import app # Logger settings @@ -27,83 +20,21 @@ def setup_logger(name, log_file=None, level=logging.INFO): logger = setup_logger("APIVendingMachine") -def token_required(f): - @wraps(f) - def decorated(*args, **kwargs): - token = None - # jwt is passed in the request header - if 'x-access-token' in request.headers: - token = request.headers['x-access-token'] - # return 401 if token is not passed - if not token: - return jsonify({'message': 'Token is missing !!'}), 401 - - try: - # decoding the payload to fetch the stored details - data = jwt.decode(token, app.config['SECRET_KEY']) - current_user = User.query \ - .filter_by(public_id=data['public_id']) \ - .first() - except: - return jsonify({ - 'message': 'Token is invalid !!' - }), 401 - # returns the current logged in users contex to the routes - return f(current_user, *args, **kwargs) - - return decorated - - -def create_tables(): - from models import db - db.drop_all() - db.create_all() - - -def fill_db_test(): - from models import db, User, Product, Role - - user1 = User(username='The First', password='pass1') - user2 = User(username='The Second', password='pass2') - user3 = User(username='The Third', password='pass3') - - user1.products.append(Product(productName='p1', amountAvailable=10, cost=5, sellerID=user1)) - user1.products.append(Product(productName='p4', amountAvailable=100, cost=20, sellerID=user1)) - user2.products.append(Product(productName='p2', amountAvailable=10, cost=10, sellerID=user2)) - user3.products.append(Product(productName='p3', amountAvailable=10, cost=15, sellerID=user3)) - - role1 = Role(id=1, roleName='buyer') - role2 = Role(id=2, roleName='seller') - - user1.roles.append(role1) - user1.roles.append(role2) - user2.roles.append(role1) - user3.roles.append(role2) - - db.session.add_all([user1, user2, user3]) - - db.session.commit() - - -def test_cost_constraint(): +def calculate_change(change): """ - Check if the cost of the product can only be entered in multiples - of 5. Will throw an IntegrityError. + Function that will return an array of + 5,10,20,50 or 100 coins as change. """ + # TODO can replace here with env variable for the + # coin types in order to change easier. - from models import db, Product - - user = db.session.query(User).all()[0] - - user.products.append(Product(productName='p1', amountAvailable=10, cost=6, sellerID=user)) - db.session.add_all([user]) - try: - db.session.commit() - except sqlalchemy.exc.IntegrityError: - logger.info("Test passed, cost can only be multiples of 5.") + coins = [] + for value in [100, 50, 20, 10, 5]: + coins.extend([value] * int(change / value)) + change = change % value + return coins if __name__ == '__main__': - create_tables() - fill_db_test() - test_cost_constraint() + change = 275 + print(calculate_change(change))