├── bin ├── cmd.py ├── __init__.py └── start_web.py ├── runtime.txt ├── server ├── services │ ├── __init__.py │ ├── weather.py │ ├── products.py │ ├── demos.py │ ├── users.py │ ├── retailers.py │ ├── distribution_centers.py │ └── shipments.py ├── tests │ ├── __init__.py │ ├── test_server_utils.py │ ├── run_unit_tests.py │ ├── utils.py │ ├── run_integration_tests.py │ ├── test_products_service.py │ ├── test_users_service.py │ ├── test_retailers_service.py │ ├── test_distribution_centers_service.py │ ├── test_demos_service.py │ └── test_shipments_service.py ├── web │ ├── rest │ │ ├── __init__.py │ │ ├── landing.py │ │ ├── root.py │ │ ├── products.py │ │ ├── weather.py │ │ ├── retailers.py │ │ ├── distribution_centers.py │ │ ├── shipments.py │ │ └── demos.py │ ├── utils.py │ └── __init__.py ├── config.py ├── __init__.py ├── utils.py └── exceptions.py ├── Procfile ├── requirements.dev.txt ├── Notice.txt ├── .coveragerc ├── run.sh ├── requirements.txt ├── template-env.local ├── manifest.yml ├── .travis.yml ├── .bluemix ├── pipeline-TEST.sh └── pipeline-DEPLOY.sh ├── .cfignore ├── .env ├── .gitignore ├── setup.py ├── sample_event.json ├── README.md ├── LICENSE └── swagger.yaml /bin/cmd.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.13 -------------------------------------------------------------------------------- /server/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/web/rest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -w 4 bin.start_web:application 2 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | coveralls==1.1.0 3 | -------------------------------------------------------------------------------- /Notice.txt: -------------------------------------------------------------------------------- 1 | This product includes software originally developed by IBM Corporation 2 | Copyright 2016 IBM Corp. -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/ 5 | *__init__* 6 | */tests/* 7 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | pip install virtualenv 2 | virtualenv venv 3 | source .env 4 | pip install -r requirements.dev.txt 5 | python bin/start_web.py 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.12.3 2 | Flask-Cors==2.1.2 3 | gunicorn==19.5.0 4 | requests==2.20.0 5 | PyJWT==1.4.0 6 | decorator==4.0.4 7 | cf-deployment-tracker==1.0.2 8 | -------------------------------------------------------------------------------- /template-env.local: -------------------------------------------------------------------------------- 1 | # Copy to .env.local 2 | # If you edit this file, you need to rerun 'source .env' 3 | export ERP_SERVICE=http://0.0.0.0:3000 4 | export OPENWHISK_AUTH= 5 | export OPENWHISK_PACKAGE=lwr 6 | -------------------------------------------------------------------------------- /server/tests/test_server_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | def suite(): 4 | test_suite = unittest.TestSuite() 5 | return test_suite 6 | 7 | class UtilsTestCase(unittest.TestCase): 8 | """Tests for `services/utils.py`.""" 9 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - path: . 4 | memory: 256M 5 | instances: 1 6 | name: acme-freight-controller 7 | host: acme-freight-controller 8 | buildpack: python_buildpack 9 | env: 10 | ACME_FREIGHT_ENV: PROD 11 | ERP_SERVICE: XXX 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: 3 | - pip 4 | python: 5 | - "2.7" 6 | # command to install dependencies 7 | install: "pip install -r requirements.dev.txt" 8 | # command to run tests 9 | before_script: 10 | - source .env 11 | script: python server/tests/run_unit_tests.py 12 | -------------------------------------------------------------------------------- /server/web/rest/landing.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for the root API 3 | """ 4 | from flask import Response, Blueprint, redirect 5 | 6 | landing_blueprint = Blueprint('landing', __name__) 7 | 8 | @landing_blueprint.route('/', methods=['GET']) 9 | def landing(): 10 | return 'This is the Acme Freight Controller API. Did you mean to visit UI instead?' 11 | -------------------------------------------------------------------------------- /.bluemix/pipeline-TEST.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Download Python dependencies 3 | sudo pip install virtualenv 4 | virtualenv venv 5 | source venv/bin/activate 6 | export PYTHONPATH=$PWD 7 | pip install -r requirements.dev.txt 8 | # Run unit and coverage tests 9 | coverage run server/tests/run_unit_tests.py 10 | if [ -z ${COVERALLS_REPO_TOKEN} ]; then 11 | echo No Coveralls token specified, skipping coveralls.io upload 12 | else 13 | COVERALLS_REPO_TOKEN=$COVERALLS_REPO_TOKEN coveralls 14 | fi 15 | -------------------------------------------------------------------------------- /server/web/rest/root.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for the root API 3 | """ 4 | from flask import Response, Blueprint 5 | 6 | root_v1_blueprint = Blueprint('root_v1_api', __name__) 7 | 8 | @root_v1_blueprint.route('/', methods=['GET']) 9 | def ping(): 10 | """ 11 | Return a health status. 12 | 13 | :return: { 14 | "status": "OK" 15 | } 16 | 17 | """ 18 | 19 | return Response('{"status": "OK"}', 20 | status=200, 21 | mimetype='application/json') -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # python virtual environment/compiled files 10 | venv/ 11 | *.pyc 12 | 13 | # Intellij/PyCharm config 14 | .idea/ 15 | 16 | # dev files 17 | requirements.dev.txt 18 | .env.local 19 | *.DS_Store 20 | 21 | # test files 22 | .coverage 23 | .coveragerc 24 | 25 | # sub-modules 26 | modules/ 27 | 28 | # SCM docs 29 | README.md 30 | .github/ 31 | .git/ 32 | .gitignore 33 | .gitmodules 34 | 35 | # API specs 36 | swagger.yml -------------------------------------------------------------------------------- /server/config.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | 3 | 4 | class Config(object): 5 | 6 | ENVIRONMENT = env.get('ACME_FREIGHT_ENV', 'DEV').upper() 7 | SECRET = env.get('SECRET', 'secret') 8 | 9 | OPENWHISK_URL = env.get('OPENWHISK_URL', 'https://openwhisk.ng.bluemix.net') 10 | OPENWHISK_AUTH = env.get('OPENWHISK_AUTH') 11 | OPENWHISK_PACKAGE = env.get('OPENWHISK_PACKAGE', 'lwr') 12 | 13 | OPENWHISK_API_KEY = env.get('OW_API_KEY') 14 | OPENWHISK_API_URL = env.get('OW_API_URL') 15 | 16 | APIC_CLIENT_ID = env.get('APIC_CLIENT_ID') 17 | APIC_CLIENT_SECRET = env.get('APIC_CLIENT_SECRET') -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parent application that loads any child applications at their proper 3 | paths. If we end up doing the static parts completely separately, this can 4 | just load the API app directly. 5 | """ 6 | from server.exceptions import AuthenticationException 7 | 8 | 9 | def create_app(): 10 | """ 11 | Wires the flask applications together into one wsgi app 12 | 13 | :return: A flask/wsgi app that is composed of multiple sub apps 14 | """ 15 | from server.web import create_app 16 | # If we do a static javascript app via flask, add it here 17 | # from server.web import create_app as create_web_app 18 | return create_app() 19 | -------------------------------------------------------------------------------- /server/web/rest/products.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for ERP product resources. 3 | """ 4 | import server.services.products as product_service 5 | from flask import g, Response, Blueprint 6 | from server.web.utils import logged_in 7 | 8 | products_v1_blueprint = Blueprint('products_v1_api', __name__) 9 | 10 | 11 | @products_v1_blueprint.route('/products', methods=['GET']) 12 | @logged_in 13 | def get_products(): 14 | """ 15 | Get all product objects. 16 | 17 | :return: [{ 18 | "id": "I9", 19 | "name": "Milk", 20 | "supplier": "Abbott" 21 | }, {...}] 22 | 23 | """ 24 | 25 | products = product_service.get_products(token=g.auth['loopback_token']) 26 | return Response(products, 27 | status=200, 28 | mimetype='application/json') 29 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | if [ -z "$LOGISTICS_WIZARD_HOME" ]; 2 | then 3 | export LOGISTICS_WIZARD_HOME=`git rev-parse --show-toplevel` 4 | fi 5 | 6 | source $LOGISTICS_WIZARD_HOME/venv/bin/activate 7 | export PYTHONPATH=$LOGISTICS_WIZARD_HOME 8 | export PORT=5000 9 | export ERP_SERVICE=http://0.0.0.0:3000 10 | export OPENWHISK_PACKAGE=lwr 11 | # Enter values if using API Connect ERP service 12 | # export APIC_CLIENT_ID= 13 | # export APIC_CLIENT_SECRET= 14 | # Enter values if using native API management on OpenWhisk actions 15 | #export OW_API_KEY= 16 | #export OW_API_URL= 17 | 18 | alias logistics_wizard="python bin/cmd.py" 19 | 20 | if [ -f "$LOGISTICS_WIZARD_HOME/.pythonrc" ]; 21 | then 22 | export PYTHONSTARTUP=$LOGISTICS_WIZARD_HOME/.pythonrc 23 | fi 24 | 25 | if [ -f ".env.local" ]; 26 | then 27 | source ./.env.local 28 | fi 29 | -------------------------------------------------------------------------------- /server/tests/run_unit_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runs all unit tests by adding them to a test suite and then executes 3 | the test suite 4 | """ 5 | import unittest 6 | from os import environ as env 7 | 8 | # Required tests 9 | test_modules = [ 10 | 'server.tests.test_server_utils' 11 | ] 12 | 13 | suite = unittest.TestSuite() 14 | 15 | for test in test_modules: 16 | try: 17 | # If the module defines a suite() function, call it to get the suite. 18 | mod = __import__(test, globals(), locals(), ['suite']) 19 | suite_func = getattr(mod, 'suite') 20 | suite.addTest(suite_func()) 21 | except (ImportError, AttributeError): 22 | # else, just load all the test cases from the module. 23 | suite.addTest(unittest.defaultTestLoader.loadTestsFromName(test)) 24 | 25 | unittest.TextTestRunner(failfast=True).run(suite) 26 | -------------------------------------------------------------------------------- /server/tests/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for use by the unit tests 3 | """ 4 | from datetime import datetime 5 | 6 | def get_bad_token(): 7 | """Returns an invalid loopback token""" 8 | return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJu" \ 9 | "YW1lIjoiU3VwcGx5IENoYWluIE1hbmFnZXIgKG1kcTNRMEZDYlEpIiwiZ" \ 10 | "GVtb0lkIjoiN2U3ZjQ1ZTA1ZTQyNTFiNWFjZDBiMTlmYTRlZDI5OTIiLC" \ 11 | "JlbWFpbCI6ImNocmlzLm1kcTNRMEZDYlFAYWNtZS5jb20iLCJyb2xlcyI" \ 12 | "6W3siY3JlYXRlZCI6IjIwMTYtMDYtMDFUMTE6MTU6MzQuNTE1WiIsImlk" \ 13 | "IjoiM2RlZjE0MzZlYjUxZTQzOWU3ZmI1MDA5ZmVjM2EwZWIiLCJtb2RpZ" \ 14 | "mllZCI6IjIwMTYtMDYtMDFUMTE6MTU6MzQuNTE1WiIsIm5hbWUiOiJzdX" \ 15 | "BwbHljaGFpbm1hbmFnZXIifV0sImlkIjoiN2U3ZjQ1ZTA1ZTQyNTFiNWF" \ 16 | "jZDBiMTlmYTRlZDQ3OTAifSwiZXhwIjoxNDY2MDQ1MzczLCJsb29wYmFj" \ 17 | "a190b2tlbiI6ImhFRnJzeGhSa3lBUEhQWWN0TWtEaE9mSTZOaDY5TlBzc" \ 18 | "FhkRWhxWXVSTzBqZDBLem1HVkZFbnpRZVRwVTV2N28ifQ.I8_iqpK7pwY" \ 19 | "5mmND220MhnsMDS5FtqRhtliEiXoMAGM" 20 | -------------------------------------------------------------------------------- /server/tests/run_integration_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runs all integration tests by adding them to a test suite and then executes 3 | the test suite 4 | """ 5 | import unittest 6 | from os import environ as env 7 | 8 | # Required tests 9 | test_modules = [ 10 | 'server.tests.test_demos_service', 11 | 'server.tests.test_distribution_centers_service', 12 | 'server.tests.test_products_service', 13 | 'server.tests.test_retailers_service', 14 | 'server.tests.test_shipments_service', 15 | 'server.tests.test_users_service' 16 | ] 17 | 18 | suite = unittest.TestSuite() 19 | 20 | for test in test_modules: 21 | try: 22 | # If the module defines a suite() function, call it to get the suite. 23 | mod = __import__(test, globals(), locals(), ['suite']) 24 | suite_func = getattr(mod, 'suite') 25 | suite.addTest(suite_func()) 26 | except (ImportError, AttributeError): 27 | # else, just load all the test cases from the module. 28 | suite.addTest(unittest.defaultTestLoader.loadTestsFromName(test)) 29 | 30 | unittest.TextTestRunner(failfast=True).run(suite) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # python virtual environment/compiled files 30 | venv/ 31 | *.pyc 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Intellij/PyCharm config 40 | .idea/ 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # Local environment 62 | .env.local 63 | 64 | # Client 65 | .DS_STORE 66 | client-src/node_modules 67 | client-src/dist 68 | client-src/coverage/ 69 | client-src/.nyc_output/ 70 | -------------------------------------------------------------------------------- /bin/start_web.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main app script to load a logistics wizard app model and either run it (DEV mode) 3 | or make it available to the parent service as a WSGI module. 4 | 5 | Configuration is done via environment variables. The current config keys 6 | are: 7 | 8 | DATABASE_URL The url to connect to the database, defaults to a sqlite 9 | file called 'test.db' in this module's directory. 10 | 11 | ENV Set the environment flag, can be DEV, TEST, or PROD. 12 | Defaults to DEV. 13 | 14 | TOKEN_SECRET The secret to be used for creating JSON Web Tokens, should 15 | be long and hard to guess, like a password. 16 | 17 | """ 18 | import os 19 | from server import create_app 20 | from server.exceptions import APIException 21 | 22 | 23 | def start_app(): 24 | """ 25 | Run in development mode, never used in production. 26 | """ 27 | port = int(os.getenv("PORT", 5000)) 28 | try: 29 | app = create_app() 30 | app.run(host='0.0.0.0', port=port) 31 | except APIException as e: 32 | print ("Application failed to start") 33 | 34 | 35 | if __name__ == "__main__": 36 | start_app() 37 | else: 38 | application = create_app() 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | A proxy app for the Logistics Wizard demo system 3 | 4 | See: 5 | https://github.com/IBM-Bluemix/cf-deployment-tracker-service 6 | https://github.com/IBM-Bluemix/cf-deployment-tracker-client-python 7 | """ 8 | 9 | # Always prefer setuptools over distutils 10 | from setuptools import setup, find_packages 11 | # To use a consistent encoding 12 | from codecs import open 13 | from os import path 14 | 15 | here = path.abspath(path.dirname(__file__)) 16 | 17 | # Get the long description from the README file 18 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 19 | long_description = f.read() 20 | 21 | setup( 22 | name='acme-freight-controller', 23 | version='0.1.0', 24 | description='Proxy app for the Acme Freight demo system', 25 | long_description=long_description, 26 | url='https://github.com/strongloop/acme-freight-controller', 27 | author='IBM-Bluemix', 28 | author_email='ibm@us.ibm.com', 29 | license='Apache-2.0', 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Programming Language :: Python :: 2.7' 35 | ], 36 | keywords='demos samples logistics hybrid-cloud microservices', 37 | packages=find_packages(), 38 | install_requires=['Flask,Flask-Cors>=2,gunicorn>=19,requests>=2,PyJWT>=1,decorator>=4,cf-deployment-tracker'], 39 | ) 40 | -------------------------------------------------------------------------------- /.bluemix/pipeline-DEPLOY.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Set app's env vars 3 | if [ "$REPO_BRANCH" == "master" ]; then 4 | ACME_FREIGHT_ENV="PROD" 5 | else 6 | ACME_FREIGHT_ENV="DEV" 7 | fi 8 | echo "ACME_FREIGHT_ENV: $ACME_FREIGHT_ENV" 9 | 10 | domain=".mybluemix.net" 11 | case "${REGION_ID}" in 12 | ibm:yp:eu-gb) 13 | domain=".eu-gb.mybluemix.net" 14 | ;; 15 | ibm:yp:au-syd) 16 | domain=".au-syd.mybluemix.net" 17 | ;; 18 | esac 19 | # Deploy app 20 | if ! cf app $CF_APP; then 21 | cf push $CF_APP -n $CF_APP --no-start 22 | cf set-env $CF_APP ACME_FREIGHT_ENV ${ACME_FREIGHT_ENV} 23 | cf set-env $CF_APP ERP_SERVICE https://$ERP_SERVICE_APP_NAME$domain 24 | cf set-env $CF_APP OPENWHISK_AUTH "${OPENWHISK_AUTH}" 25 | cf set-env $CF_APP OPENWHISK_PACKAGE ${RECOMMENDATION_PACKAGE_NAME} 26 | cf set-env $CF_APP OW_API_KEY "${OW_API_KEY}" 27 | cf set-env $CF_APP OW_API_URL "${OW_API_URL}" 28 | cf start $CF_APP 29 | else 30 | OLD_CF_APP=${CF_APP}-OLD-$(date +"%s") 31 | rollback() { 32 | set +e 33 | if cf app $OLD_CF_APP; then 34 | cf logs $CF_APP --recent 35 | cf delete $CF_APP -f 36 | cf rename $OLD_CF_APP $CF_APP 37 | fi 38 | exit 1 39 | } 40 | set -e 41 | trap rollback ERR 42 | cf rename $CF_APP $OLD_CF_APP 43 | cf push $CF_APP -n $CF_APP --no-start 44 | cf set-env $CF_APP ACME_FREIGHT_ENV ${ACME_FREIGHT_ENV} 45 | cf set-env $CF_APP ERP_SERVICE https://$ERP_SERVICE_APP_NAME$domain 46 | cf set-env $CF_APP OPENWHISK_AUTH "${OPENWHISK_AUTH}" 47 | cf set-env $CF_APP OPENWHISK_PACKAGE ${RECOMMENDATION_PACKAGE_NAME} 48 | cf set-env $CF_APP OW_API_KEY "${OW_API_KEY}" 49 | cf set-env $CF_APP OW_API_URL "${OW_API_URL}" 50 | cf start $CF_APP 51 | cf delete $OLD_CF_APP -f 52 | fi 53 | -------------------------------------------------------------------------------- /sample_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "cf63821f-9521-3f27-92ec-e1562ccd469b", 3 | "class": "bulletin", 4 | "msg_type_cd": 2, 5 | "msg_type": "Update", 6 | "pil": "NPW", 7 | "phenomena": "HT", 8 | "significance": "Y", 9 | "etn": "0003", 10 | "office_cd": "KSGX", 11 | "office_name": "Nebraska", 12 | "office_st_cd": "NE", 13 | "office_cntry_cd": "US", 14 | "event_desc": "Snow Storm", 15 | "severity_cd": 3, 16 | "severity": "Moderate", 17 | "categories": [ 18 | { 19 | "category": "Met", 20 | "category_cd": 2 21 | } 22 | ], 23 | "response_types": [ 24 | { 25 | "response_type": "Execute", 26 | "response_type_cd": 4 27 | } 28 | ], 29 | "urgency": "Expected", 30 | "urgency_cd": 2, 31 | "certainty": "Likely", 32 | "certainty_cd": 2, 33 | "effective_dt_tm_local": null, 34 | "effective_dt_tm_tz_abbrv": null, 35 | "expire_dt_tm_local": "2016-06-29T20:00:00-07:00", 36 | "expire_dt_tm_tz_abbrv": "EST", 37 | "expire_time_gmt": 1467255600, 38 | "onset_dt_tm_local": null, 39 | "onset_dt_tm_tz_abbrv": null, 40 | "flood": null, 41 | "area_type": "Z", 42 | "lat": 40.80, 43 | "lon": -76.68, 44 | "radiusInKm": 700, 45 | "area_id": "CAZ048", 46 | "area_name": "Lincoln Area", 47 | "st_cd": "NE", 48 | "st_name": "Nebraska", 49 | "cntry_cd": "US", 50 | "cntry_name": "UNITED STATES OF AMERICA", 51 | "headline_text": "Heavy snow fall thru next week", 52 | "detail_key": "cf63821f-9521-3f27-92ec-e1562ccd469b", 53 | "source": "National Weather Service", 54 | "disclaimer": null, 55 | "issue_dt_tm_local": "2016-06-27T04:38:50-07:00", 56 | "issue_dt_tm_tz_abbrv": "EST", 57 | "identifier": "e234b6f889bf9a91c5d9f309db63eaa0", 58 | "proc_dt_tm_local": "2016-06-27T04:38:58-07:00", 59 | "proc_dt_tm_tz_abbrv": "EST" 60 | } 61 | -------------------------------------------------------------------------------- /server/services/weather.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the weather resource. 3 | """ 4 | import json 5 | import requests 6 | from server.utils import call_openwhisk 7 | from server.exceptions import ResourceDoesNotExistException, APIException 8 | 9 | def get_recommendations(demoGuid): 10 | """ 11 | Get recommendations 12 | """ 13 | 14 | try: 15 | payload = dict() 16 | payload['demoGuid'] = demoGuid 17 | response = call_openwhisk('retrieve', payload) 18 | except Exception as e: 19 | raise APIException('KO', internal_details=str(e)) 20 | 21 | return response 22 | 23 | def acknowledge_recommendation(demoGuid, recommendationId): 24 | """ 25 | Acknowledge the given recommendation 26 | """ 27 | 28 | try: 29 | payload = dict() 30 | payload['demoGuid'] = demoGuid 31 | payload['recommendationId'] = recommendationId 32 | response = call_openwhisk('acknowledge', payload) 33 | except Exception as e: 34 | raise APIException('KO', internal_details=str(e)) 35 | 36 | return response 37 | 38 | def trigger_simulation(demoGuid): 39 | """ 40 | Trigger a simulation in the given demo 41 | Creates a Snow Storm 42 | """ 43 | 44 | try: 45 | payload = dict() 46 | payload['demoGuid'] = demoGuid 47 | event = dict() 48 | event = json.loads(open('./sample_event.json').read()) 49 | payload['event'] = event 50 | response = call_openwhisk('recommend', payload) 51 | except Exception as e: 52 | raise APIException('KO', internal_details=str(e)) 53 | 54 | return response 55 | 56 | def get_observations(latitude, longitude): 57 | """ 58 | Return observations for the given location 59 | """ 60 | 61 | try: 62 | payload = dict() 63 | payload['latitude'] = latitude 64 | payload['longitude'] = longitude 65 | response = call_openwhisk('observations', payload) 66 | except Exception as e: 67 | raise APIException('KO', internal_details=str(e)) 68 | 69 | return response 70 | -------------------------------------------------------------------------------- /server/web/rest/weather.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for the Weather Recommendation API 3 | """ 4 | import server.services.weather as weather_service 5 | from flask import g, request, Response, Blueprint 6 | from server.web.utils import get_json_data, check_null_input, logged_in 7 | 8 | weather_v1_blueprint = Blueprint('weather_v1_api', __name__) 9 | 10 | @weather_v1_blueprint.route('/weather/recommendations', methods=['GET']) 11 | @logged_in 12 | def get_recommendations(): 13 | """ 14 | Return recommendations. 15 | 16 | :return: the list of recommendations 17 | 18 | """ 19 | recommendations = weather_service.get_recommendations(g.auth['guid']) 20 | return Response(recommendations, 21 | status=200, 22 | mimetype='application/json') 23 | 24 | @weather_v1_blueprint.route('/weather/acknowledge', methods=['POST']) 25 | @logged_in 26 | def acknowledge_recommendation(): 27 | """ 28 | Acknowledge a recommendation. 29 | """ 30 | body = get_json_data(request) 31 | 32 | response = weather_service.acknowledge_recommendation(g.auth['guid'], body.get('id')) 33 | return Response(response, 34 | status=200, 35 | mimetype='application/json') 36 | 37 | @weather_v1_blueprint.route('/weather/simulate', methods=['POST']) 38 | @logged_in 39 | def trigger_simulation(): 40 | """ 41 | Trigger a simulation for the given demo. 42 | """ 43 | response = weather_service.trigger_simulation(g.auth['guid']) 44 | return Response(response, 45 | status=200, 46 | mimetype='application/json') 47 | 48 | @weather_v1_blueprint.route('/weather/observations', methods=['POST']) 49 | @logged_in 50 | def get_observations(): 51 | """ 52 | Return observations for the given location. 53 | :return: observations for the given location. 54 | """ 55 | body = get_json_data(request) 56 | 57 | observations = weather_service.get_observations(body.get('latitude'), body.get('longitude')) 58 | return Response(observations, 59 | status=200, 60 | mimetype='application/json') 61 | -------------------------------------------------------------------------------- /server/services/products.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the product resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the product 5 | object and should just call into the service layer to act upon a product resource. 6 | """ 7 | import requests 8 | import json 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import (APIException, 12 | AuthenticationException) 13 | 14 | ########################### 15 | # Utilities # 16 | ########################### 17 | 18 | 19 | def product_to_dict(product): 20 | """ 21 | Convert an instance of the Product model to a dict. 22 | 23 | :param product: An instance of the Product model. 24 | :return: A dict representing the product. 25 | """ 26 | return { 27 | 'id': product.id, 28 | 'name': product.name, 29 | 'supplierId': product.supplierId 30 | } 31 | 32 | 33 | ########################### 34 | # Services # 35 | ########################### 36 | 37 | def get_products(token): 38 | """ 39 | Get a list of products from the ERP system. 40 | 41 | :param token: The ERP Loopback session token. 42 | 43 | :return: The list of existing products. 44 | """ 45 | 46 | # Create and format request to ERP 47 | url = '%s/api/v1/Products' % get_service_url('lw-erp') 48 | headers = { 49 | 'cache-control': "no-cache", 50 | 'Authorization': token 51 | } 52 | headers.update(get_apic_credentials()) 53 | 54 | try: 55 | response = requests.request("GET", url, headers=headers) 56 | except Exception as e: 57 | raise APIException('ERP threw error retrieving products', internal_details=str(e)) 58 | 59 | # Check for possible errors in response 60 | if response.status_code == 401: 61 | raise AuthenticationException('ERP access denied', 62 | internal_details=json.loads(response.text).get('error').get('message')) 63 | 64 | return response.text 65 | -------------------------------------------------------------------------------- /server/tests/test_products_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads 3 | import server.tests.utils as utils 4 | import server.services.demos as demo_service 5 | import server.services.users as user_service 6 | import server.services.products as product_service 7 | from server.exceptions import AuthenticationException 8 | 9 | 10 | def suite(): 11 | test_suite = unittest.TestSuite() 12 | test_suite.addTest(GetProductsTestCase('test_get_products_success')) 13 | test_suite.addTest(GetProductsTestCase('test_get_products_invalid_token')) 14 | return test_suite 15 | 16 | 17 | ########################### 18 | # Unit Tests # 19 | ########################### 20 | 21 | class GetProductsTestCase(unittest.TestCase): 22 | """Tests for `services/products.py - get_products()`.""" 23 | 24 | def setUp(self): 25 | # Create demo 26 | self.demo = demo_service.create_demo() 27 | demo_json = loads(self.demo) 28 | demo_guid = demo_json.get('guid') 29 | demo_user_id = demo_json.get('users')[0].get('id') 30 | 31 | # Log in user 32 | auth_data = user_service.login(demo_guid, demo_user_id) 33 | self.loopback_token = auth_data.get('loopback_token') 34 | 35 | def test_get_products_success(self): 36 | """With correct values, are valid products returned?""" 37 | 38 | # Get products 39 | products = product_service.get_products(self.loopback_token) 40 | 41 | # TODO: Update to use assertIsInstance(a,b) 42 | # Check all expected object values are present 43 | products_json = loads(products) 44 | # Check that the products are valid 45 | for product_json in products_json: 46 | self.assertTrue(product_json.get('id')) 47 | self.assertTrue(product_json.get('name')) 48 | self.assertTrue(product_json.get('supplierId')) 49 | 50 | def test_get_products_invalid_token(self): 51 | """With an invalid token, are correct errors thrown?""" 52 | 53 | # Attempt to get products with invalid token 54 | self.assertRaises(AuthenticationException, 55 | product_service.get_products, 56 | utils.get_bad_token()) 57 | 58 | def tearDown(self): 59 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /server/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | High level utilities, can be used by any of the layers (data, service, 3 | interface) and should not have any dependency on Flask or request context. 4 | """ 5 | import requests, base64 6 | from types import FunctionType 7 | from os import environ as env 8 | import json 9 | from server.config import Config 10 | from server.exceptions import APIException 11 | 12 | 13 | def async_helper(args): 14 | """ 15 | Calls the passed in function with the input arguments. Used to mitigate 16 | calling different functions during multiprocessing 17 | 18 | :param args: Function and its arguments 19 | :return: Result of the called function 20 | """ 21 | 22 | # Isolate function arguments in their own tuple and then call the function 23 | func_args = tuple(y for y in args if type(y) != FunctionType) 24 | return args[0](*func_args) 25 | 26 | 27 | def get_service_url(service_name): 28 | """ 29 | Retrieves the URL of the service being called based on the environment 30 | that the controller is currently being run. 31 | 32 | :param service_name: Name of the service being retrieved 33 | :return: The endpoint of the input service name 34 | """ 35 | 36 | if service_name == 'lw-erp': 37 | return env['ERP_SERVICE'] 38 | else: 39 | raise APIException('Unrecognized service invocation') 40 | 41 | def get_apic_credentials(): 42 | creds = {} 43 | if Config.APIC_CLIENT_ID and Config.APIC_CLIENT_SECRET: 44 | creds['X-IBM-Client-Id'] = Config.APIC_CLIENT_ID 45 | creds['X-IBM-Client-Secret'] = Config.APIC_CLIENT_SECRET 46 | return creds 47 | else: 48 | return {} 49 | 50 | def call_openwhisk(action, payload=None): 51 | """ 52 | Calls and waits for the completion of an OpenWhisk action with the optional payload 53 | 54 | :param action: The action to call 55 | :param payload: An optional dictionary with arguments for the action 56 | :return: The invocation result 57 | """ 58 | 59 | url = '%s/api/v1/namespaces/_/actions/%s/%s?blocking=true' % ( 60 | Config.OPENWHISK_URL, 61 | Config.OPENWHISK_PACKAGE, 62 | action 63 | ) 64 | 65 | if payload is not None: 66 | payload_json = json.dumps(payload) 67 | else: 68 | payload_json = None 69 | 70 | headers = { 71 | 'Authorization': "Basic %s" % base64.b64encode(Config.OPENWHISK_AUTH), 72 | 'content-type': "application/json", 73 | 'cache-control': "no-cache" 74 | } 75 | 76 | if Config.OPENWHISK_API_URL and Config.OPENWHISK_API_KEY: 77 | del headers['Authorization'] 78 | headers['X-IBM-Client-ID'] = Config.OPENWHISK_API_KEY 79 | url = Config.OPENWHISK_API_URL.rstrip('/') + '/' + action 80 | response = requests.request("POST", url, data=payload_json, headers=headers) 81 | return response 82 | 83 | response = requests.request("POST", url, data=payload_json, headers=headers) 84 | body = json.loads(response.text) 85 | result = body.get('response').get('result') 86 | return json.dumps(result) 87 | -------------------------------------------------------------------------------- /server/web/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module containing utilities that are helpful within the Flask context. 3 | Anything that may apply for all interfaces. This may depend on the current 4 | Flask app and request. 5 | """ 6 | import jwt 7 | from server.config import Config 8 | from decorator import decorator 9 | from flask import g, request 10 | from server.exceptions import (AuthorizationException, 11 | TokenException, 12 | ValidationException) 13 | 14 | ########################### 15 | # Request Utils # 16 | ########################### 17 | 18 | 19 | def get_token_from_request(): 20 | """ 21 | Pulls the auth token from the request headers. 22 | 23 | :return: Auth token if it exists, else None. 24 | """ 25 | token = None 26 | try: 27 | header = request.headers.get('Authorization') 28 | token = header.split()[1] if header is not None else request.cookies.get('auth_token') 29 | except (AttributeError, IndexError): 30 | pass 31 | 32 | if token is None: 33 | raise TokenException('Unable to get token from request.') 34 | 35 | return token 36 | 37 | 38 | @decorator 39 | def logged_in(func, *args, **kwargs): 40 | if g.auth is None: 41 | raise AuthorizationException('No existing session.') 42 | else: 43 | return func(*args, **kwargs) 44 | 45 | 46 | def request_wants_json(): 47 | best = request.accept_mimetypes.best_match(['text/html', 'application/json']) 48 | return best == 'application/json' and \ 49 | request.accept_mimetypes[best] > \ 50 | request.accept_mimetypes['text/html'] 51 | 52 | 53 | def get_json_data(req): 54 | """ 55 | Takes a request and extracts the JSON from the body 56 | 57 | :param req: An API request 58 | :return: A JSON object. 59 | """ 60 | try: 61 | return req.get_json() 62 | except Exception: 63 | raise ValidationException('No JSON payload received in the request') 64 | 65 | 66 | def check_null_input(*fields): 67 | """ 68 | Checks a list of params and raises a ValidationException if None 69 | 70 | :param fields: List of (param, error) tuples to check for null values 71 | """ 72 | for field in fields: 73 | if field[0] is None: 74 | raise ValidationException('You must specify a %s' % field[1]) 75 | 76 | 77 | def compose_error(exc, e): 78 | """ 79 | Composes an error to return to the client after APIException is thrown 80 | 81 | :param exc: Raised exception 82 | :param e: Returned error 83 | :return: An string to specify the error if param is None 84 | """ 85 | return_error = dict(code=exc.status_code, 86 | message=e.message) 87 | 88 | if hasattr(e, 'user_details') and e.user_details is not None: 89 | return_error['user_details'] = e.user_details 90 | 91 | return return_error 92 | 93 | 94 | def tokenize(data): 95 | """ 96 | Creates a signed JSON Web Token of the data. 97 | 98 | :param data: Any dict to be tokenized. 99 | :return: A signed token representing the data. 100 | """ 101 | return jwt.encode(data, Config.SECRET) 102 | 103 | 104 | def detokenize(token): 105 | """ 106 | Convert a token to a dict. Raises a TokenException if the token is 107 | expired or tampered with. 108 | 109 | :param token: The token to be converted. 110 | :return: 111 | """ 112 | try: 113 | data = jwt.decode(token, Config.SECRET) 114 | except Exception as e: 115 | raise TokenException('Error decoding JWT token.', internal_details=str(e)) 116 | 117 | return data 118 | -------------------------------------------------------------------------------- /server/web/rest/retailers.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for ERP retailer resources. 3 | """ 4 | import server.services.retailers as retailer_service 5 | import server.services.shipments as shipment_service 6 | from flask import g, request, Response, Blueprint 7 | from server.web.utils import logged_in, check_null_input 8 | 9 | retailers_v1_blueprint = Blueprint('retailers_v1_api', __name__) 10 | 11 | 12 | @retailers_v1_blueprint.route('/retailers', methods=['GET']) 13 | @logged_in 14 | def get_retailers(): 15 | """ 16 | Get all retailer objects. 17 | 18 | :return: [{ 19 | "id": "123", 20 | "address": {Address} 21 | }, {...}] 22 | 23 | """ 24 | 25 | retailers = retailer_service.get_retailers(token=g.auth['loopback_token']) 26 | return Response(retailers, 27 | status=200, 28 | mimetype='application/json') 29 | 30 | 31 | @retailers_v1_blueprint.route('/retailers/', methods=['GET']) 32 | @logged_in 33 | def get_retailer(retailer_id): 34 | """ 35 | Retrieve a single retailer object. 36 | 37 | :param retailer_id: The retailer's id 38 | 39 | :return: { 40 | "id": "123", 41 | "address": {Address} 42 | } 43 | 44 | """ 45 | check_null_input((retailer_id, 'retailer to retrieve')) 46 | 47 | retailer = retailer_service.get_retailer(token=g.auth['loopback_token'], 48 | retailer_id=retailer_id) 49 | return Response(retailer, 50 | status=200, 51 | mimetype='application/json') 52 | 53 | 54 | @retailers_v1_blueprint.route('/retailers//shipments', methods=['GET']) 55 | @logged_in 56 | def get_retailer_shipments(retailer_id): 57 | """ 58 | Retrieve all shipments heading to the specified retailer. 59 | 60 | :param retailer_id: The retailer's id 61 | 62 | :return: [{ 63 | "id": "123", 64 | "status": "SHIPPED", 65 | "createdAt": "2015-11-05T22:00:51.692765", 66 | "updatedAt": "2015-11-08T22:00:51.692765", 67 | "deliveredAt": "2015-11-08T22:00:51.692765", 68 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 69 | "currentLocation": {Address}, 70 | "fromId": "D2", 71 | "toId:": "123" 72 | }, {...}] 73 | 74 | """ 75 | check_null_input((retailer_id, 'retailer whose shipments you want to retrieve')) 76 | status = request.args.get('status') 77 | 78 | shipments = shipment_service.get_shipments(token=g.auth['loopback_token'], 79 | retailer_id=retailer_id, 80 | status=status) 81 | return Response(shipments, 82 | status=200, 83 | mimetype='application/json') 84 | 85 | 86 | @retailers_v1_blueprint.route('/retailers//inventory', methods=['GET']) 87 | @logged_in 88 | def get_retailer_inventory(retailer_id): 89 | """ 90 | Retrieve all inventory at the specified retailer. 91 | 92 | :param retailer_id: The retailer's id 93 | 94 | :return: [{ 95 | "id": "123", 96 | "quantity": 10, 97 | "productId": "123", 98 | "locationId": "123", 99 | "locationType": "Retailer" 100 | }, {...}] 101 | """ 102 | check_null_input((retailer_id, 'retailer whose inventory you want to retrieve')) 103 | 104 | inventory = retailer_service.get_retailer_inventory(token=g.auth['loopback_token'], 105 | retailer_id=retailer_id) 106 | return Response(inventory, 107 | status=200, 108 | mimetype='application/json') 109 | -------------------------------------------------------------------------------- /server/web/rest/distribution_centers.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for ERP distribution center resources. 3 | """ 4 | import server.services.distribution_centers as distribution_center_service 5 | import server.services.shipments as shipment_service 6 | from flask import g, request, Response, Blueprint 7 | from server.web.utils import logged_in, check_null_input 8 | 9 | distribution_centers_v1_blueprint = Blueprint('distribution_centers_v1_api', __name__) 10 | 11 | 12 | @distribution_centers_v1_blueprint.route('/distribution-centers', methods=['GET']) 13 | @logged_in 14 | def get_distribution_centers(): 15 | """ 16 | Get all distribution center objects. 17 | 18 | :return: [{ 19 | "id": "D2", 20 | "address": {Address}, 21 | "contact": {Contact} 22 | }, {...}] 23 | 24 | """ 25 | 26 | distribution_centers = distribution_center_service.get_distribution_centers(token=g.auth['loopback_token']) 27 | return Response(distribution_centers, 28 | status=200, 29 | mimetype='application/json') 30 | 31 | 32 | @distribution_centers_v1_blueprint.route('/distribution-centers/', methods=['GET']) 33 | @logged_in 34 | def get_distribution_center(dc_id): 35 | """ 36 | Retrieve a single distribution center object. 37 | 38 | :param dc_id: The distribution center's id 39 | 40 | :return: { 41 | "id": "D2", 42 | "address": {Address}, 43 | "contact": {Contact} 44 | } 45 | 46 | """ 47 | check_null_input((dc_id, 'distribution center to retrieve')) 48 | 49 | distribution_center = distribution_center_service.get_distribution_center(token=g.auth['loopback_token'], 50 | dc_id=dc_id) 51 | return Response(distribution_center, 52 | status=200, 53 | mimetype='application/json') 54 | 55 | 56 | @distribution_centers_v1_blueprint.route('/distribution-centers//shipments', methods=['GET']) 57 | @logged_in 58 | def get_distribution_centers_shipments(dc_id): 59 | """ 60 | Retrieve all shipments originating from the specified distribution center. 61 | 62 | :param dc_id: The distribution center's id 63 | 64 | :return: [{ 65 | "id": "123", 66 | "status": "SHIPPED", 67 | "createdAt": "2015-11-05T22:00:51.692765", 68 | "updatedAt": "2015-11-08T22:00:51.692765", 69 | "deliveredAt": "2015-11-08T22:00:51.692765", 70 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 71 | "currentLocation": {Address}, 72 | "fromId": "123", 73 | "toId:": "123" 74 | }, {...}] 75 | 76 | """ 77 | check_null_input((dc_id, 'distribution center whose shipments you want to retrieve')) 78 | status = request.args.get('status') 79 | 80 | shipments = shipment_service.get_shipments(token=g.auth['loopback_token'], 81 | dc_id=dc_id, 82 | status=status) 83 | return Response(shipments, 84 | status=200, 85 | mimetype='application/json') 86 | 87 | 88 | @distribution_centers_v1_blueprint.route('/distribution-centers//inventory', methods=['GET']) 89 | @logged_in 90 | def get_distribution_center_inventory(dc_id): 91 | """ 92 | Retrieve all inventory at the specified distribution center. 93 | 94 | :param dc_id: The distribution center's id 95 | 96 | :return: [{ 97 | "id": "123", 98 | "quantity": 10, 99 | "productId": "123", 100 | "locationId": "123", 101 | "locationType": "DistributionCenter" 102 | }, {...}] 103 | """ 104 | check_null_input((dc_id, 'distribution center whose inventory you want to retrieve')) 105 | 106 | inventory = distribution_center_service.get_distribution_center_inventory(token=g.auth['loopback_token'], 107 | dc_id=dc_id) 108 | return Response(inventory, 109 | status=200, 110 | mimetype='application/json') 111 | -------------------------------------------------------------------------------- /server/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global exception registry 3 | """ 4 | 5 | 6 | class APIException(Exception): 7 | """ 8 | Generic Exception wrapper 9 | """ 10 | 11 | status_code = 500 12 | 13 | def __init__(self, message, user_details=None, internal_details=None): 14 | """ 15 | Create a new APIException 16 | 17 | :param message: General exception message 18 | :param user_details: Message to be shown to user 19 | :param internal_details: Additional details provided by the system 20 | """ 21 | self.message = message 22 | self.internal_details = internal_details 23 | if user_details is not None: 24 | self.user_details = user_details 25 | else: 26 | self.user_details = self.message 27 | 28 | super(APIException, self).__init__(self, message) 29 | 30 | def __str__(self): 31 | exception_str = super(APIException, self).__str__() 32 | dict_str = str(self.__dict__) 33 | return '{0} {1}'.format(exception_str, dict_str) 34 | 35 | def __unicode__(self): 36 | exception_str = super(APIException, self).__unicode__() 37 | dict_str = unicode(self.__dict__) 38 | return u'{0} {1}'.format(exception_str, dict_str) 39 | 40 | def to_dict(self): 41 | """ 42 | Convert this exception to a dict for serialization. 43 | """ 44 | return { 45 | 'error': self.user_details 46 | } 47 | 48 | 49 | class TokenException(APIException): 50 | """ 51 | Raised when a token fails to be decoded because it is either tampered 52 | with or expired. 53 | """ 54 | 55 | status_code = 400 56 | 57 | def __init__(self, message, user_details=None, internal_details=None): 58 | super(TokenException, self).__init__( 59 | message, user_details=user_details, internal_details=internal_details) 60 | 61 | 62 | class ValidationException(APIException): 63 | """ 64 | Indicates an exception when validating input data. 65 | """ 66 | 67 | status_code = 400 68 | 69 | def __init__(self, message, user_details=None, internal_details=None): 70 | super(ValidationException, self).__init__( 71 | message, user_details=user_details, internal_details=internal_details) 72 | 73 | 74 | class UnprocessableEntityException(APIException): 75 | """ 76 | Indicates an exception when valid input is semantically incorrect. 77 | """ 78 | 79 | status_code = 422 80 | 81 | def __init__(self, message, user_details=None, internal_details=None): 82 | super(UnprocessableEntityException, self).__init__( 83 | message, user_details=user_details, internal_details=internal_details) 84 | 85 | 86 | class IntegrityException(APIException): 87 | """ 88 | Raised when database constraints are not met on updates. 89 | """ 90 | 91 | status_code = 409 92 | 93 | def __init__(self, message, user_details=None, internal_details=None): 94 | super(IntegrityException, self).__init__( 95 | message, user_details=user_details, 96 | internal_details=internal_details) 97 | 98 | 99 | class ResourceDoesNotExistException(APIException): 100 | """ 101 | Raised when retrieving a resource and it cannot be found in the database. 102 | """ 103 | 104 | status_code = 404 105 | 106 | def __init__(self, user_details=None, internal_details=None, *args): 107 | if len(args) > 0: 108 | message = args[0] 109 | else: 110 | message = 'Resource does not exist.' 111 | super(ResourceDoesNotExistException, self).__init__( 112 | message, user_details=user_details, 113 | internal_details=internal_details) 114 | 115 | 116 | class AuthenticationException(APIException): 117 | """ 118 | Raised when authentication fails for any reason. 119 | """ 120 | 121 | status_code = 401 122 | 123 | def __init__(self, message, user_details=None, internal_details=None): 124 | super(AuthenticationException, self).__init__( 125 | message, user_details=user_details, 126 | internal_details=internal_details) 127 | 128 | 129 | class AuthorizationException(APIException): 130 | """ 131 | Raised when a user tries to access something without proper permissions. 132 | """ 133 | 134 | status_code = 401 135 | 136 | def __init__(self, message, user_details=None, internal_details=None): 137 | super(AuthorizationException, self).__init__( 138 | message, user_details=user_details, 139 | internal_details=internal_details) 140 | -------------------------------------------------------------------------------- /server/web/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initializer for the API application. This will create a new Flask app and 3 | register all interface versions (Blueprints), initialize the database and 4 | register app level error handlers. 5 | """ 6 | import re 7 | import json 8 | import atexit 9 | 10 | from flask import Flask, current_app, Response 11 | from flask.ext.cors import CORS 12 | from server.web.utils import compose_error 13 | 14 | 15 | def create_app(): 16 | """ 17 | Create the api as it's own app so that it's easier to scale it out on it's 18 | own in the future. 19 | 20 | :return: A flask object/wsgi callable. 21 | """ 22 | import cf_deployment_tracker 23 | from server.config import Config 24 | from os import environ as env 25 | from server.exceptions import APIException 26 | from server.web.utils import request_wants_json 27 | from server.web.rest.landing import landing_blueprint 28 | from server.web.rest.root import root_v1_blueprint 29 | from server.web.rest.demos import demos_v1_blueprint, setup_auth_from_request 30 | from server.web.rest.shipments import shipments_v1_blueprint 31 | from server.web.rest.distribution_centers import distribution_centers_v1_blueprint 32 | from server.web.rest.retailers import retailers_v1_blueprint 33 | from server.web.rest.products import products_v1_blueprint 34 | from server.web.rest.weather import weather_v1_blueprint 35 | 36 | # Emit Bluemix deployment event 37 | cf_deployment_tracker.track() 38 | 39 | # Create the app 40 | logistics_wizard = Flask('logistics_wizard', static_folder=None) 41 | CORS(logistics_wizard, origins=[re.compile('.*')], supports_credentials=True) 42 | if Config.ENVIRONMENT == 'DEV': 43 | logistics_wizard.debug = True 44 | 45 | # Register the blueprints for each component 46 | logistics_wizard.register_blueprint(landing_blueprint, url_prefix='/') 47 | logistics_wizard.register_blueprint(root_v1_blueprint, url_prefix='/api/v1') 48 | logistics_wizard.register_blueprint(demos_v1_blueprint, url_prefix='/api/v1') 49 | logistics_wizard.register_blueprint(shipments_v1_blueprint, url_prefix='/api/v1') 50 | logistics_wizard.register_blueprint(distribution_centers_v1_blueprint, url_prefix='/api/v1') 51 | logistics_wizard.register_blueprint(retailers_v1_blueprint, url_prefix='/api/v1') 52 | logistics_wizard.register_blueprint(products_v1_blueprint, url_prefix='/api/v1') 53 | logistics_wizard.register_blueprint(weather_v1_blueprint, url_prefix='/api/v1') 54 | 55 | logistics_wizard.before_request(setup_auth_from_request) 56 | 57 | def exception_handler(e): 58 | """ 59 | Handle any exception thrown in the interface layer and return 60 | a JSON response with the error details. Wraps python exceptions 61 | with a generic exception message. 62 | 63 | :param e: The raised exception. 64 | :return: A Flask response object. 65 | """ 66 | if not isinstance(e, APIException): 67 | exc = APIException(u'Server Error', 68 | internal_details=unicode(e)) 69 | else: 70 | exc = e 71 | current_app.logger.error(exc) 72 | return Response(json.dumps(compose_error(exc, e)), 73 | status=exc.status_code, 74 | mimetype='application/json') 75 | 76 | def not_found_handler(e): 77 | current_app.logger.exception(e) 78 | if request_wants_json(): 79 | status_code = 404 80 | return Response(json.dumps({ 81 | 'code': status_code, 82 | 'message': 'Resource not found.' 83 | }), 84 | status=status_code, 85 | mimetype='application/json') 86 | else: 87 | # TODO: Default to the root web page 88 | # return index() 89 | pass 90 | 91 | def bad_request_handler(e): 92 | current_app.logger.exception(e) 93 | status_code = 400 94 | return Response(json.dumps({ 95 | 'code': status_code, 96 | 'message': 'Bad request.' 97 | }), 98 | status=status_code, 99 | mimetype='application/json') 100 | 101 | # Register error handlers 102 | logistics_wizard.errorhandler(Exception)(exception_handler) 103 | logistics_wizard.errorhandler(400)(bad_request_handler) 104 | logistics_wizard.errorhandler(404)(not_found_handler) 105 | 106 | return logistics_wizard 107 | -------------------------------------------------------------------------------- /server/services/demos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the demo resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the demo 5 | object and should just call into the service layer to act upon a demo resource. 6 | """ 7 | import requests 8 | import json 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import (ResourceDoesNotExistException) 12 | from server.exceptions import (APIException, 13 | UnprocessableEntityException) 14 | 15 | ########################### 16 | # Utilities # 17 | ########################### 18 | 19 | 20 | def demo_to_dict(demo): 21 | """ 22 | Convert an instance of the Demo model to a dict. 23 | 24 | :param demo: An instance of the Demo model. 25 | :return: A dict representing the demo. 26 | """ 27 | return { 28 | 'id': demo.id, 29 | 'guid': demo.guid, 30 | 'createdAt': demo.createdAt, 31 | 'users': demo.users 32 | } 33 | 34 | 35 | ########################### 36 | # Services # 37 | ########################### 38 | 39 | def create_demo(): 40 | """ 41 | Create a new demo session in the ERP system. 42 | 43 | :return: The created Demo model. 44 | """ 45 | 46 | # Create and format request to ERP 47 | url = '%s/api/v1/Demos' % get_service_url('lw-erp') 48 | headers = { 49 | 'content-type': "application/json", 50 | 'cache-control': "no-cache" 51 | } 52 | 53 | headers.update(get_apic_credentials()) 54 | 55 | try: 56 | response = requests.request("POST", url, headers=headers) 57 | except Exception as e: 58 | raise APIException('ERP threw error creating new Demo', internal_details=str(e)) 59 | 60 | return response.text 61 | 62 | 63 | def get_demo_by_guid(guid): 64 | """ 65 | Retrieve a demo from the ERP system by guid. 66 | 67 | :param guid: The demo's guid. 68 | 69 | :return: An instance of the Demo. 70 | """ 71 | 72 | # Create and format request to ERP 73 | url = '%s/api/v1/Demos/findByGuid/%s' % (get_service_url('lw-erp'), guid) 74 | headers = {'cache-control': "no-cache"} 75 | headers.update(get_apic_credentials()) 76 | 77 | try: 78 | response = requests.request("GET", url, headers=headers) 79 | except Exception as e: 80 | raise APIException('ERP threw error retrieving demo', internal_details=str(e)) 81 | 82 | # Check for possible errors in response 83 | if response.status_code == 404: 84 | raise ResourceDoesNotExistException('Demo does not exist', 85 | internal_details=json.loads(response.text).get('error').get('message')) 86 | 87 | return response.text 88 | 89 | 90 | def delete_demo_by_guid(guid): 91 | """ 92 | Delete a demo from the ERP system by guid. 93 | 94 | :param guid: The demo's guid. 95 | """ 96 | 97 | # Create and format request to ERP 98 | url = '%s/api/v1/Demos/%s' % (get_service_url('lw-erp'), guid) 99 | headers = get_apic_credentials() 100 | 101 | try: 102 | response = requests.request("DELETE", url, headers=headers) 103 | except Exception as e: 104 | raise APIException('ERP threw error deleting demo', internal_details=str(e)) 105 | 106 | # Check for possible errors in response 107 | if response.status_code == 404: 108 | raise ResourceDoesNotExistException('Demo does not exist', 109 | internal_details=json.loads(response.text).get('error').get('message')) 110 | 111 | return 112 | 113 | 114 | def get_demo_retailers(guid): 115 | """ 116 | Retrieve retailers for a demo in the ERP system by guid. 117 | 118 | :param guid: The demo's guid. 119 | :return: An instance of the Demo. 120 | """ 121 | 122 | # Create and format request to ERP 123 | url = '%s/api/v1/Demos/%s/retailers' % (get_service_url('lw-erp'), guid) 124 | headers = {'cache-control': "no-cache"} 125 | headers.update(get_apic_credentials()) 126 | 127 | try: 128 | response = requests.request("GET", url, headers=headers) 129 | except Exception as e: 130 | raise APIException('ERP threw error retrieving retailers for demo', 131 | internal_details=str(e)) 132 | 133 | # Check for possible errors in response 134 | if response.status_code == 404: 135 | raise ResourceDoesNotExistException('Demo does not exist', 136 | internal_details=json.loads(response.text).get('error').get('message')) 137 | 138 | return response.text 139 | -------------------------------------------------------------------------------- /server/services/users.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the user resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the user 5 | object and should just call into the service layer to act upon a user resource. 6 | """ 7 | import json 8 | import requests 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import ResourceDoesNotExistException, APIException 12 | 13 | 14 | ########################### 15 | # Utilities # 16 | ########################### 17 | 18 | 19 | def user_to_dict(user): 20 | """ 21 | Convert an instance of the User model to a dict. 22 | 23 | :param user: An instance of the User model. 24 | :return: A dict representing the user. 25 | """ 26 | return { 27 | 'id': user.id, 28 | 'demoId': user.demoId, 29 | 'email': user.email, 30 | 'username': user.username, 31 | 'roles': user.roles 32 | } 33 | 34 | 35 | ########################### 36 | # Services # 37 | ########################### 38 | 39 | 40 | def create_user(guid, retailer_id): 41 | """ 42 | Create a new user in the ERP system. 43 | 44 | :param guid: The demo's guid 45 | :param retailer_id: Retailer the user will be associated with. 46 | 47 | :return: The created User model. 48 | """ 49 | 50 | # Create and format request to ERP 51 | url = '%s/api/v1/Demos/%s/createUser' % (get_service_url('lw-erp'), guid) 52 | headers = { 53 | 'content-type': "application/json", 54 | 'cache-control': "no-cache" 55 | } 56 | headers.update(get_apic_credentials()) 57 | 58 | payload = dict() 59 | payload['retailerId'] = int(retailer_id) 60 | payload_json = json.dumps(payload) 61 | 62 | try: 63 | response = requests.request("POST", url, data=payload_json, headers=headers) 64 | except Exception as e: 65 | raise APIException('ERP threw error creating new user for demo', internal_details=str(e)) 66 | 67 | # Check for possible errors in response 68 | if response.status_code == 404: 69 | raise ResourceDoesNotExistException('Demo or retailer does not exist', 70 | internal_details=json.loads(response.text).get('error').get('message')) 71 | 72 | return response.text 73 | 74 | 75 | def login(guid, user_id): 76 | """ 77 | Authenticate a user against the ERP system. 78 | 79 | :param guid: The demo guid being logged in for. 80 | :param user_id: The user_id for which to log in. 81 | :return: Auth data returned by ERP system 82 | """ 83 | 84 | # Create and format request to ERP 85 | url = '%s/api/v1/Demos/%s/loginAs' % (get_service_url('lw-erp'), guid) 86 | headers = { 87 | 'content-type': "application/json", 88 | 'cache-control': "no-cache" 89 | } 90 | headers.update(get_apic_credentials()) 91 | payload = dict() 92 | payload['userId'] = int(user_id) 93 | payload_json = json.dumps(payload) 94 | 95 | try: 96 | response = requests.request("POST", url, data=payload_json, headers=headers) 97 | except Exception as e: 98 | raise APIException('ERP threw error creating new user for demo', internal_details=str(e)) 99 | 100 | # Check for possible errors in response 101 | if response.status_code == 404: 102 | raise ResourceDoesNotExistException('Demo or user does not exist', 103 | internal_details=json.loads(response.text).get('error').get('message')) 104 | 105 | login_response = json.loads(response.text) 106 | return { 107 | 'loopback_token': login_response.get('token').get('id'), 108 | 'user': login_response.get('user'), 109 | 'guid': guid 110 | } 111 | 112 | 113 | def logout(token): 114 | """ 115 | Log a user out of the system. 116 | 117 | :param token: The ERP Loopback session token 118 | """ 119 | 120 | # Create and format request to ERP 121 | url = '%s/api/v1/Users/logout' % get_service_url('lw-erp') 122 | headers = { 123 | 'content-type': "application/json", 124 | 'Authorization': token 125 | } 126 | headers.update(get_apic_credentials()) 127 | 128 | try: 129 | response = requests.request("POST", url, headers=headers) 130 | except Exception as e: 131 | raise APIException('ERP threw error creating new user for demo', internal_details=str(e)) 132 | 133 | # Check for possible errors in response 134 | if response.status_code == 500: 135 | raise ResourceDoesNotExistException('Session does not exist', 136 | internal_details=json.loads(response.text).get('error').get('message')) 137 | 138 | return 139 | -------------------------------------------------------------------------------- /server/services/retailers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the retailer resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the retailer 5 | object and should just call into the service layer to act upon a retailer resource. 6 | """ 7 | import requests 8 | import json 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import (ResourceDoesNotExistException) 12 | from server.exceptions import (APIException, 13 | AuthenticationException, 14 | UnprocessableEntityException) 15 | 16 | ########################### 17 | # Utilities # 18 | ########################### 19 | 20 | 21 | def retailer_to_dict(retailer): 22 | """ 23 | Convert an instance of the Retailer model to a dict. 24 | 25 | :param retailer: An instance of the Retailer model. 26 | :return: A dict representing the retailer. 27 | """ 28 | return { 29 | 'id': retailer.id, 30 | 'address': retailer.address 31 | } 32 | 33 | 34 | ########################### 35 | # Services # 36 | ########################### 37 | 38 | def get_retailers(token): 39 | """ 40 | Get a list of retailers from the ERP system. 41 | 42 | :param token: The ERP Loopback session token. 43 | 44 | :return: The list of existing retailers. 45 | """ 46 | 47 | # Create and format request to ERP 48 | url = '%s/api/v1/Retailers' % get_service_url('lw-erp') 49 | headers = { 50 | 'cache-control': "no-cache", 51 | 'Authorization': token 52 | } 53 | headers.update(get_apic_credentials()) 54 | 55 | try: 56 | response = requests.request("GET", url, headers=headers) 57 | except Exception as e: 58 | raise APIException('ERP threw error retrieving retailers', internal_details=str(e)) 59 | 60 | # Check for possible errors in response 61 | if response.status_code == 401: 62 | raise AuthenticationException('ERP access denied', 63 | internal_details=json.loads(response.text).get('error').get('message')) 64 | 65 | return response.text 66 | 67 | 68 | def get_retailer(token, retailer_id): 69 | """ 70 | Get a retailer from the ERP system. 71 | 72 | :param token: The ERP Loopback session token. 73 | :param retailer_id: The ID of the retailer to be retrieved. 74 | 75 | :return: The retrieved retailer. 76 | """ 77 | 78 | # Create and format request to ERP 79 | url = '%s/api/v1/Retailers/%s' % (get_service_url('lw-erp'), str(retailer_id)) 80 | headers = { 81 | 'cache-control': "no-cache", 82 | 'Authorization': token 83 | } 84 | headers.update(get_apic_credentials()) 85 | 86 | try: 87 | response = requests.request("GET", url, headers=headers) 88 | except Exception as e: 89 | raise APIException('ERP threw error retrieving retailer', internal_details=str(e)) 90 | 91 | # Check for possible errors in response 92 | if response.status_code == 401: 93 | raise AuthenticationException('ERP access denied', 94 | internal_details=json.loads(response.text).get('error').get('message')) 95 | elif response.status_code == 404: 96 | raise ResourceDoesNotExistException('Retailer does not exist', 97 | internal_details=json.loads(response.text).get('error').get('message')) 98 | 99 | return response.text 100 | 101 | 102 | def get_retailer_inventory(token, retailer_id): 103 | """ 104 | Get a retailer from the ERP system. 105 | 106 | :param token: The ERP Loopback session token. 107 | :param retailer_id: The ID of the retailer for which inventory is to be be retrieved. 108 | 109 | :return: The retrieved retailer's inventory. 110 | """ 111 | 112 | # Create and format request to ERP 113 | url = '%s/api/v1/Retailers/%s/inventories' % (get_service_url('lw-erp'), str(retailer_id)) 114 | headers = { 115 | 'cache-control': "no-cache", 116 | 'Authorization': token 117 | } 118 | headers.update(get_apic_credentials()) 119 | 120 | try: 121 | response = requests.request("GET", url, headers=headers) 122 | except Exception as e: 123 | raise APIException('ERP threw error retrieving retailer inventory', internal_details=str(e)) 124 | 125 | # Check for possible errors in response 126 | if response.status_code == 401: 127 | raise AuthenticationException('ERP access denied', 128 | internal_details=json.loads(response.text).get('error').get('message')) 129 | elif response.status_code == 404: 130 | raise ResourceDoesNotExistException('Retailer does not exist', 131 | internal_details=json.loads(response.text).get('error').get('message')) 132 | 133 | return response.text 134 | -------------------------------------------------------------------------------- /server/services/distribution_centers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the distribution center resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the distribution center 5 | object and should just call into the service layer to act upon a distribution center resource. 6 | """ 7 | import requests 8 | import json 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import (APIException, 12 | AuthenticationException, 13 | ResourceDoesNotExistException) 14 | 15 | ########################### 16 | # Utilities # 17 | ########################### 18 | 19 | 20 | def distribution_center_to_dict(distribution_center): 21 | """ 22 | Convert an instance of the Distribution Center model to a dict. 23 | 24 | :param distribution_center: An instance of the Distribution Center model. 25 | :return: A dict representing the distribution center. 26 | """ 27 | return { 28 | 'id': distribution_center.id, 29 | 'address': distribution_center.address, 30 | 'contact': distribution_center.contact 31 | } 32 | 33 | 34 | ########################### 35 | # Services # 36 | ########################### 37 | 38 | def get_distribution_centers(token): 39 | """ 40 | Get a list of distribution centers from the ERP system. 41 | 42 | :param token: The ERP Loopback session token. 43 | 44 | :return: The list of existing distribution centers. 45 | """ 46 | 47 | # Create and format request to ERP 48 | url = '%s/api/v1/DistributionCenters' % get_service_url('lw-erp') 49 | headers = { 50 | 'cache-control': "no-cache", 51 | 'Authorization': token 52 | } 53 | headers.update(get_apic_credentials()) 54 | 55 | try: 56 | response = requests.request("GET", url, headers=headers) 57 | except Exception as e: 58 | raise APIException('ERP threw error retrieving distribution centers', internal_details=str(e)) 59 | 60 | # Check for possible errors in response 61 | if response.status_code == 401: 62 | raise AuthenticationException('ERP access denied', 63 | internal_details=json.loads(response.text).get('error').get('message')) 64 | 65 | return response.text 66 | 67 | 68 | def get_distribution_center(token, dc_id): 69 | """ 70 | Get a distribution center from the ERP system. 71 | 72 | :param token: The ERP Loopback session token. 73 | :param dc_id: The ID of the distribution center to be retrieved. 74 | 75 | :return: The retrieved distribution center. 76 | """ 77 | 78 | # Create and format request to ERP 79 | url = '%s/api/v1/DistributionCenters/%s' % (get_service_url('lw-erp'), str(dc_id)) 80 | headers = { 81 | 'cache-control': "no-cache", 82 | 'Authorization': token 83 | } 84 | headers.update(get_apic_credentials()) 85 | 86 | try: 87 | response = requests.request("GET", url, headers=headers) 88 | except Exception as e: 89 | raise APIException('ERP threw error retrieving distribution center', internal_details=str(e)) 90 | 91 | # Check for possible errors in response 92 | if response.status_code == 401: 93 | raise AuthenticationException('ERP access denied', 94 | internal_details=json.loads(response.text).get('error').get('message')) 95 | elif response.status_code == 404: 96 | raise ResourceDoesNotExistException('Distribution center does not exist', 97 | internal_details=json.loads(response.text).get('error').get('message')) 98 | 99 | return response.text 100 | 101 | 102 | def get_distribution_center_inventory(token, dc_id): 103 | """ 104 | Get a distribution center from the ERP system. 105 | 106 | :param token: The ERP Loopback session token. 107 | :param dc_id: The ID of the distribution center for which inventory is to be be retrieved. 108 | 109 | :return: The retrieved distribution center's inventory. 110 | """ 111 | 112 | # Create and format request to ERP 113 | url = '%s/api/v1/DistributionCenters/%s/inventories' % (get_service_url('lw-erp'), str(dc_id)) 114 | headers = { 115 | 'cache-control': "no-cache", 116 | 'Authorization': token 117 | } 118 | headers.update(get_apic_credentials()) 119 | 120 | try: 121 | response = requests.request("GET", url, headers=headers) 122 | except Exception as e: 123 | raise APIException('ERP threw error retrieving distribution center inventory', internal_details=str(e)) 124 | 125 | # Check for possible errors in response 126 | if response.status_code == 401: 127 | raise AuthenticationException('ERP access denied', 128 | internal_details=json.loads(response.text).get('error').get('message')) 129 | elif response.status_code == 404: 130 | raise ResourceDoesNotExistException('Distribution center does not exist', 131 | internal_details=json.loads(response.text).get('error').get('message')) 132 | 133 | return response.text 134 | -------------------------------------------------------------------------------- /server/web/rest/shipments.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for ERP shipment resources. 3 | """ 4 | import json 5 | 6 | import server.services.shipments as shipment_service 7 | from flask import g, request, Response, Blueprint 8 | from server.web.utils import logged_in 9 | from server.web.utils import get_json_data, check_null_input 10 | 11 | shipments_v1_blueprint = Blueprint('shipments_v1_api', __name__) 12 | 13 | 14 | @shipments_v1_blueprint.route('/shipments', methods=['GET']) 15 | @logged_in 16 | def get_shipments(): 17 | """ 18 | Get all shipment objects. 19 | 20 | :return: [{ 21 | "id": "123", 22 | "status": "SHIPPED", 23 | "createdAt": "2015-11-05T22:00:51.692765", 24 | "updatedAt": "2015-11-08T22:00:51.692765", 25 | "deliveredAt": "2015-11-08T22:00:51.692765", 26 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 27 | "currentLocation": {Address}, 28 | "fromId": "D2", 29 | "toId:": "123" 30 | }, {...}] 31 | 32 | """ 33 | 34 | status = request.args.get('status') 35 | retailer_id = request.args.get('rid') 36 | dc_id = request.args.get('did') 37 | shipments = shipment_service.get_shipments(token=g.auth['loopback_token'], 38 | status=status, 39 | retailer_id=retailer_id, 40 | dc_id=dc_id) 41 | return Response(shipments, 42 | status=200, 43 | mimetype='application/json') 44 | 45 | 46 | @shipments_v1_blueprint.route('/shipments', methods=['POST']) 47 | @logged_in 48 | def create_shipment(): 49 | """ 50 | Create a new shipment object. 51 | 52 | :param { 53 | "status": "NEW", 54 | "estimatedTimeOfArrival": "2016-07-10T00:00:00.000Z", 55 | "fromId": "D2", 56 | "toId": "123" 57 | } 58 | 59 | :return: { 60 | "id": "123", 61 | "status": "ACCEPTED", 62 | "createdAt": "2015-11-05T22:00:51.692765", 63 | "updatedAt": "2015-11-08T22:00:51.692765", 64 | "deliveredAt": "2015-11-08T22:00:51.692765", 65 | "estimatedTimeOfArrival": "2016-07-10T00:00:00.000Z", 66 | "currentLocation": {Address}, 67 | "fromId": "D2", 68 | "toId:": "123" 69 | } 70 | 71 | """ 72 | 73 | # Get inputs and make sure required params are not null 74 | data = get_json_data(request) 75 | 76 | shipment = shipment_service.create_shipment(token=g.auth['loopback_token'], shipment=data) 77 | return Response(shipment, 78 | status=201, 79 | mimetype='application/json') 80 | 81 | 82 | @shipments_v1_blueprint.route('/shipments/', methods=['GET']) 83 | @logged_in 84 | def get_shipment(shipment_id): 85 | """ 86 | Retrieve a single shipment object. 87 | 88 | :param shipment_id: The shipment's id 89 | 90 | :return: { 91 | "id": "123", 92 | "status": "SHIPPED", 93 | "createdAt": "2015-11-05T22:00:51.692765", 94 | "updatedAt": "2015-11-08T22:00:51.692765", 95 | "deliveredAt": "2015-11-08T22:00:51.692765", 96 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 97 | "currentLocation": {Address}, 98 | "fromId": "123", 99 | "toId:": "123", 100 | "items": [{LineItem}] 101 | } 102 | 103 | """ 104 | include_items = request.args.get('include_items') 105 | check_null_input((shipment_id, 'shipment to retrieve')) 106 | 107 | shipment = shipment_service.get_shipment(token=g.auth['loopback_token'], 108 | shipment_id=shipment_id, 109 | include_items=include_items) 110 | 111 | return Response(shipment, 112 | status=200, 113 | mimetype='application/json') 114 | 115 | 116 | @shipments_v1_blueprint.route('/shipments/', methods=['DELETE']) 117 | @logged_in 118 | def delete_shipment(shipment_id): 119 | """ 120 | Retrieve a single shipment object. 121 | 122 | :param shipment_id: The shipment's id 123 | :return: 124 | 125 | """ 126 | check_null_input((shipment_id, 'shipment to delete')) 127 | 128 | shipment_service.delete_shipment(token=g.auth['loopback_token'], shipment_id=shipment_id) 129 | return '', 204 130 | 131 | 132 | @shipments_v1_blueprint.route('/shipments/', methods=['PUT']) 133 | @logged_in 134 | def update_shipment(shipment_id): 135 | """ 136 | Update a single shipment object. 137 | 138 | :param shipment_id: The shipment's id 139 | :param { 140 | "id": "123", 141 | "status": "SHIPPED", 142 | "createdAt": "2015-11-05T22:00:51.692765", 143 | "updatedAt": "2015-11-08T22:00:51.692765", 144 | "deliveredAt": "2015-11-08T22:00:51.692765", 145 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 146 | "currentLocation": {Address}, 147 | "fromId": "D2", 148 | "toId:": "123" 149 | } 150 | 151 | :return: { 152 | "id": "123", 153 | "status": "SHIPPED", 154 | "createdAt": "2015-11-05T22:00:51.692765", 155 | "updatedAt": "2015-11-08T22:00:51.692765", 156 | "deliveredAt": "2015-11-08T22:00:51.692765", 157 | "estimatedTimeOfArrival": "2015-11-07T22:00:51.692765", 158 | "currentLocation": {Address}, 159 | "fromId": "D2", 160 | "toId:": "123" 161 | } 162 | 163 | """ 164 | check_null_input((shipment_id, 'shipment to update')) 165 | 166 | updated_shipment = get_json_data(request) 167 | shipment = shipment_service.update_shipment(token=g.auth['loopback_token'], 168 | shipment_id=shipment_id, shipment=updated_shipment) 169 | return Response(shipment, 170 | status=200, 171 | mimetype='application/json') 172 | -------------------------------------------------------------------------------- /server/tests/test_users_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads 3 | from datetime import datetime, timedelta 4 | import server.tests.utils as test_utils 5 | import server.web.utils as web_utils 6 | import server.services.demos as demo_service 7 | import server.services.users as user_service 8 | from server.exceptions import ResourceDoesNotExistException 9 | 10 | def suite(): 11 | test_suite = unittest.TestSuite() 12 | test_suite.addTest(CreateUserTestCase('test_user_create_success')) 13 | test_suite.addTest(CreateUserTestCase('test_user_create_invalid_inputs')) 14 | test_suite.addTest(UserLoginTestCase('test_user_login_success')) 15 | test_suite.addTest(UserLoginTestCase('test_user_login_invalid_inputs')) 16 | test_suite.addTest(UserLogoutTestCase('test_user_logout_success')) 17 | test_suite.addTest(UserLogoutTestCase('test_user_logout_invalid_token')) 18 | test_suite.addTest(TokenizeTestCase('test_tokenize_and_detokenize')) 19 | return test_suite 20 | 21 | 22 | ########################### 23 | # Unit Tests # 24 | ########################### 25 | 26 | class CreateUserTestCase(unittest.TestCase): 27 | """Tests for `services/users.py - create_user()`.""" 28 | 29 | def setUp(self): 30 | # Create demo 31 | self.demo = demo_service.create_demo() 32 | self.retailers = demo_service.get_demo_retailers(loads(self.demo).get('guid')) 33 | 34 | def test_user_create_success(self): 35 | """With correct values, is a valid user returned?""" 36 | # Create new user assigned to the first retailer 37 | user = user_service.create_user(loads(self.demo).get('guid'), 38 | loads(self.retailers)[0].get('id')) 39 | 40 | # TODO: Update to use assertIsInstance(a,b) 41 | # Check all expected object values are present 42 | user_json = loads(user) 43 | self.assertTrue(user_json.get('id')) 44 | self.assertTrue(user_json.get('demoId')) 45 | self.assertTrue(user_json.get('email')) 46 | self.assertTrue(user_json.get('username')) 47 | 48 | def test_user_create_invalid_inputs(self): 49 | """With invalid inputs, are correct errors thrown?""" 50 | 51 | # Attempt to create user with invalid inputs 52 | # Invalid demo guid 53 | self.assertRaises(ResourceDoesNotExistException, 54 | user_service.create_user, 55 | '123321', loads(self.retailers)[0].get('id')) 56 | # Invalid retailer id 57 | self.assertRaises(ResourceDoesNotExistException, 58 | user_service.create_user, 59 | loads(self.demo).get('guid'), '123321') 60 | 61 | def tearDown(self): 62 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 63 | 64 | 65 | class UserLoginTestCase(unittest.TestCase): 66 | """Tests for `services/users.py - login()`.""" 67 | 68 | def setUp(self): 69 | # Create demo 70 | self.demo = demo_service.create_demo() 71 | 72 | def test_user_login_success(self): 73 | """With correct values, is a valid user logged in?""" 74 | 75 | # Log in user 76 | demo_json = loads(self.demo) 77 | auth_data = user_service.login(demo_json.get('guid'), 78 | demo_json.get('users')[0].get('id')) 79 | 80 | # TODO: Update to use assertIsInstance(a,b) 81 | # Check all expected object values are present 82 | self.assertTrue(auth_data.get('loopback_token')) 83 | self.assertTrue(auth_data.get('user')) 84 | 85 | user_json = auth_data.get('user') 86 | self.assertTrue(user_json.get('id')) 87 | self.assertTrue(user_json.get('demoId')) 88 | self.assertTrue(user_json.get('username')) 89 | self.assertTrue(user_json.get('email')) 90 | 91 | if user_json.get('roles'): 92 | for role_json in user_json.get('roles'): 93 | self.assertTrue(role_json.get('id')) 94 | self.assertTrue(role_json.get('name')) 95 | self.assertTrue(role_json.get('created')) 96 | self.assertTrue(role_json.get('modified')) 97 | 98 | def test_user_login_invalid_inputs(self): 99 | """With invalid inputs, are correct errors thrown?""" 100 | 101 | demo_json = loads(self.demo) 102 | self.assertRaises(ResourceDoesNotExistException, 103 | user_service.login, 104 | '123321', demo_json.get('users')[0].get('id')) 105 | self.assertRaises(ResourceDoesNotExistException, 106 | user_service.login, 107 | demo_json.get('guid'), '123321') 108 | 109 | def tearDown(self): 110 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 111 | 112 | 113 | class UserLogoutTestCase(unittest.TestCase): 114 | """Tests for `services/users.py - logout()`.""" 115 | 116 | def setUp(self): 117 | # Create demo 118 | self.demo = demo_service.create_demo() 119 | demo_json = loads(self.demo) 120 | demo_guid = demo_json.get('guid') 121 | demo_user_id = demo_json.get('users')[0].get('id') 122 | 123 | # Log in user 124 | auth_data = user_service.login(demo_guid, demo_user_id) 125 | self.loopback_token = auth_data.get('loopback_token') 126 | 127 | def test_user_logout_success(self): 128 | """With correct values, is a valid user logged out?""" 129 | 130 | self.assertTrue(user_service.logout(self.loopback_token) is None) 131 | 132 | def test_user_logout_invalid_token(self): 133 | """With an invalid token, are correct errors thrown?""" 134 | 135 | self.assertRaises(ResourceDoesNotExistException, 136 | user_service.logout, 137 | test_utils.get_bad_token()) 138 | 139 | def tearDown(self): 140 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 141 | 142 | 143 | class TokenizeTestCase(unittest.TestCase): 144 | """Tests for `services/users.py - get_token_for_user() and get_auth_from_token()`.""" 145 | 146 | def test_tokenize_and_detokenize(self): 147 | """Is auth data correctly tokenized and later detokenized?""" 148 | 149 | # Create demo 150 | demo = demo_service.create_demo() 151 | demo_json = loads(demo) 152 | demo_guid = demo_json.get('guid') 153 | demo_user_id = demo_json.get('users')[0].get('id') 154 | 155 | # Log in user and tokenize auth data 156 | auth_data = user_service.login(demo_guid, demo_user_id) 157 | auth_data['exp'] = datetime.utcnow() + timedelta(days=14) 158 | token = web_utils.tokenize(auth_data) 159 | 160 | # Detokenize auth data 161 | decrypted_auth_data = web_utils.detokenize(token) 162 | 163 | # Check that decrypted data is equivalent to auth data 164 | self.assertTrue(auth_data.get('loopback_token') == 165 | decrypted_auth_data.get('loopback_token')) 166 | self.assertTrue(auth_data.get('exp') == 167 | decrypted_auth_data.get('exp')) 168 | self.assertTrue(auth_data.get('user').get('id') == 169 | decrypted_auth_data.get('user').get('id')) 170 | self.assertTrue(auth_data.get('guid') == 171 | decrypted_auth_data.get('guid')) 172 | 173 | # Destroy demo 174 | demo_service.delete_demo_by_guid(demo_guid) 175 | 176 | if __name__ == '__main__': 177 | unittest.main() 178 | -------------------------------------------------------------------------------- /server/web/rest/demos.py: -------------------------------------------------------------------------------- 1 | """ 2 | The REST interface for demo session resources. 3 | """ 4 | import json 5 | from datetime import datetime, timedelta 6 | from flask import g, request, Response, Blueprint 7 | from multiprocessing import Pool 8 | import server.services.demos as demo_service 9 | import server.services.users as user_service 10 | import server.services.shipments as shipment_service 11 | import server.services.distribution_centers as distribution_center_service 12 | import server.services.retailers as retailer_service 13 | import server.web.utils as web_utils 14 | from server.exceptions import (TokenException, 15 | ResourceDoesNotExistException, 16 | APIException) 17 | from server.utils import async_helper 18 | 19 | demos_v1_blueprint = Blueprint('demos_v1_api', __name__) 20 | 21 | 22 | def setup_auth_from_request(): 23 | """ 24 | Get the Auth data from the request headers and store on the g 25 | object for later use. 26 | """ 27 | try: 28 | token = web_utils.get_token_from_request() 29 | if token is not None: 30 | g.auth = web_utils.detokenize(token) 31 | except (TokenException, ResourceDoesNotExistException): 32 | g.auth = None 33 | 34 | 35 | @demos_v1_blueprint.route('/demos', methods=['POST']) 36 | def create_demo(): 37 | """ 38 | Create a new demo resource. 39 | 40 | :return: { 41 | "id": "123", 42 | "guid": "JDJhJDEdTRUR...VBrbW9vcj3k4L2sy", 43 | "createdAt": "2015-11-05T22:00:51.692765", 44 | "users": [{User}...{User}] 45 | } 46 | 47 | """ 48 | 49 | demo = demo_service.create_demo() 50 | return Response(demo, 51 | status=201, 52 | mimetype='application/json') 53 | 54 | 55 | @demos_v1_blueprint.route('/demos/', methods=['GET']) 56 | def get_demo(guid): 57 | """ 58 | Retrieve a single demo object. 59 | 60 | :param guid: The demo's guid 61 | 62 | :return: { 63 | "id": "123", 64 | "name": "Example Demo Name", 65 | "guid": "JDJhJDEdTRUR...VBrbW9vcj3k4L2sy", 66 | "createdAt": "2015-11-05T22:00:51.692765", 67 | "users": [{User}...{User}] 68 | } 69 | """ 70 | web_utils.check_null_input((guid, 'demo to retrieve')) 71 | 72 | demo = demo_service.get_demo_by_guid(guid) 73 | return Response(demo, 74 | status=200, 75 | mimetype='application/json') 76 | 77 | 78 | @demos_v1_blueprint.route('/demos/', methods=['DELETE']) 79 | def delete_demo(guid): 80 | """ 81 | Delete a demo object and all its children. 82 | 83 | :param guid: The demo's guid 84 | :return: 85 | """ 86 | web_utils.check_null_input((guid, 'demo to delete')) 87 | 88 | demo_service.delete_demo_by_guid(guid) 89 | return '', 204 90 | 91 | 92 | @demos_v1_blueprint.route('/demos//retailers', methods=['GET']) 93 | def get_demo_retailers(guid): 94 | """ 95 | Retrieve a single demo's list of retailers. 96 | 97 | :param guid: The demo's guid 98 | 99 | :return: [{ 100 | "id": "123", 101 | "address": { 102 | "city": "Raleigh", 103 | "state": "North Carolina", 104 | "country": "US", 105 | "latitude": 35.71, 106 | "longitude": -78.63 107 | }, 108 | "managerId": "123" 109 | }, {...}] 110 | """ 111 | web_utils.check_null_input((guid, 'demo for which to retrieve retailers')) 112 | 113 | retailers = demo_service.get_demo_retailers(guid) 114 | return Response(retailers, 115 | status=200, 116 | mimetype='application/json') 117 | 118 | 119 | @demos_v1_blueprint.route('/demos//users', methods=['POST']) 120 | def create_demo_user(guid): 121 | """ 122 | Create a new user for a single demo 123 | 124 | :param guid: The demo's guid 125 | :param { 126 | "retailerId": "123" 127 | } 128 | 129 | :return: { 130 | "id": "123", 131 | "demoId": "123", 132 | "username": "Retail Store Manager (XXX)", 133 | "email": "ruth.XXX@acme.com" 134 | } 135 | """ 136 | 137 | # Get inputs and make sure required params are not null 138 | data = web_utils.get_json_data(request) 139 | retailer_id = data.get('retailerId') 140 | web_utils.check_null_input((guid, 'demo for which to create a user'), 141 | (retailer_id, 'retailer to make a user for the demo')) 142 | 143 | user = user_service.create_user(guid, retailer_id) 144 | return Response(user, 145 | status=201, 146 | mimetype='application/json') 147 | 148 | 149 | @demos_v1_blueprint.route('/demos//login', methods=['POST']) 150 | def demo_login(guid): 151 | """ 152 | Login to a demo as a specific user 153 | 154 | :param { 155 | "userId": "123" 156 | } 157 | 158 | :return: { 159 | "token": "eyJhbGciOi...WT2aGgjY5JHvCsbA" 160 | } 161 | """ 162 | data = request.get_json() 163 | user_id = data.get('userId') 164 | web_utils.check_null_input((user_id, 'username when logging in'), 165 | (guid, 'demo guid when logging in')) 166 | 167 | # Login through the ERP system and create a JWT valid for 2 weeks 168 | auth_data = user_service.login(guid, user_id) 169 | auth_data['exp'] = datetime.utcnow() + timedelta(days=14) 170 | token = web_utils.tokenize(auth_data) 171 | resp = Response(json.dumps({'token': token}), 172 | status=200, 173 | mimetype='application/json') 174 | 175 | resp.set_cookie('auth_token', token, httponly=True) 176 | return resp 177 | 178 | 179 | @demos_v1_blueprint.route('/logout/', methods=['DELETE']) 180 | def deauthenticate(token): 181 | """ 182 | Logout the current user 183 | :param token Current web token 184 | :return: 185 | """ 186 | request_token = web_utils.get_token_from_request() 187 | # Only allow deletion of a web token if the token belongs to the current user 188 | if request_token == token: 189 | user_service.logout(token=g.auth['loopback_token']) 190 | return '', 204 191 | 192 | 193 | @demos_v1_blueprint.route('/admin', methods=['GET']) 194 | @web_utils.logged_in 195 | def load_admin_data(): 196 | """ 197 | Load all data relative to the currently logged in user 198 | 199 | :return: { 200 | "shipments": [{Shipments}], 201 | "retailers": [{Retailer}], 202 | "distribution_centers": [{Distribution Center}] 203 | } 204 | """ 205 | 206 | # Specify functions and corresponding arguments to call to retrieve ERP data 207 | loopback_token = g.auth['loopback_token'] 208 | erp_calls = [(shipment_service.get_shipments, loopback_token), 209 | (distribution_center_service.get_distribution_centers, loopback_token), 210 | (retailer_service.get_retailers, loopback_token)] 211 | pool = Pool(processes=len(erp_calls)) 212 | 213 | # Asynchronously make calls and then wait on all processes to finish 214 | try: 215 | results = pool.map(async_helper, erp_calls) 216 | except Exception as e: 217 | raise APIException('Error retrieving admin data view', internal_details=str(e)) 218 | pool.close() 219 | pool.join() 220 | 221 | # Send back serialized results to client 222 | return Response(json.dumps({ 223 | "shipments": json.loads(results[0]), 224 | "distribution-centers": json.loads(results[1]), 225 | "retailers": json.loads(results[2]) 226 | }), 227 | status=200, 228 | mimetype='application/json') 229 | -------------------------------------------------------------------------------- /server/tests/test_retailers_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads 3 | from types import IntType 4 | import server.tests.utils as utils 5 | import server.services.demos as demo_service 6 | import server.services.users as user_service 7 | import server.services.retailers as retailer_service 8 | from server.exceptions import (AuthenticationException, 9 | ResourceDoesNotExistException) 10 | 11 | 12 | def suite(): 13 | test_suite = unittest.TestSuite() 14 | test_suite.addTest(GetRetailersTestCase('test_retailers_success')) 15 | test_suite.addTest(GetRetailersTestCase('test_get_retailers_invalid_token')) 16 | test_suite.addTest(GetRetailerTestCase('test_get_retailer_success')) 17 | test_suite.addTest(GetRetailerTestCase('test_get_retailer_invalid_input')) 18 | test_suite.addTest(GetRetailerTestCase('test_get_retailer_invalid_token')) 19 | test_suite.addTest(GetRetailerInventoryTestCase('test_get_retailer_inventory_success')) 20 | test_suite.addTest(GetRetailerInventoryTestCase('test_get_retailer_inventory_invalid_input')) 21 | test_suite.addTest(GetRetailerInventoryTestCase('test_get_retailer_inventory_invalid_token')) 22 | return test_suite 23 | 24 | 25 | ########################### 26 | # Unit Tests # 27 | ########################### 28 | 29 | class GetRetailersTestCase(unittest.TestCase): 30 | """Tests for `services/retailers.py - get_retailers()`.""" 31 | 32 | def setUp(self): 33 | # Create demo 34 | self.demo = demo_service.create_demo() 35 | demo_json = loads(self.demo) 36 | demo_guid = demo_json.get('guid') 37 | demo_user_id = demo_json.get('users')[0].get('id') 38 | 39 | # Log in user 40 | auth_data = user_service.login(demo_guid, demo_user_id) 41 | self.loopback_token = auth_data.get('loopback_token') 42 | 43 | def test_retailers_success(self): 44 | """With correct values, are valid retailers returned?""" 45 | 46 | # Get retailers 47 | retailers = retailer_service.get_retailers(self.loopback_token) 48 | 49 | # TODO: Update to use assertIsInstance(a,b) 50 | # Check all expected object values are present 51 | retailers_json = loads(retailers) 52 | # Check that the retailers are valid 53 | for retailer_json in retailers_json: 54 | self.assertTrue(retailer_json.get('id')) 55 | 56 | # Check that retailer address is valid, if present 57 | if retailer_json.get('address'): 58 | self.assertTrue(retailer_json.get('address').get('city')) 59 | self.assertTrue(retailer_json.get('address').get('state')) 60 | self.assertTrue(retailer_json.get('address').get('country')) 61 | self.assertTrue(retailer_json.get('address').get('latitude')) 62 | self.assertTrue(retailer_json.get('address').get('longitude')) 63 | 64 | def test_get_retailers_invalid_token(self): 65 | """With an invalid token, are correct errors thrown?""" 66 | 67 | self.assertRaises(AuthenticationException, 68 | retailer_service.get_retailers, 69 | utils.get_bad_token()) 70 | 71 | def tearDown(self): 72 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 73 | 74 | 75 | class GetRetailerTestCase(unittest.TestCase): 76 | """Tests for `services/retailers.py - get_retailers()`.""" 77 | 78 | def setUp(self): 79 | # Create demo 80 | self.demo = demo_service.create_demo() 81 | demo_json = loads(self.demo) 82 | demo_guid = demo_json.get('guid') 83 | demo_user_id = demo_json.get('users')[0].get('id') 84 | 85 | # Log in user 86 | auth_data = user_service.login(demo_guid, demo_user_id) 87 | self.loopback_token = auth_data.get('loopback_token') 88 | 89 | def test_get_retailer_success(self): 90 | """With correct values, is a valid distribution center returned?""" 91 | 92 | # Get retailer 93 | retailers = retailer_service.get_retailers(self.loopback_token) 94 | retailer_id = loads(retailers)[0].get('id') 95 | retailer = retailer_service.get_retailer(self.loopback_token, retailer_id) 96 | 97 | # TODO: Update to use assertIsInstance(a,b) 98 | # Check all expected object values are present 99 | retailer_json = loads(retailer) 100 | # Check that the retailer is valid 101 | self.assertTrue(retailer_json.get('id')) 102 | 103 | # Check that retailer address is valid, if present 104 | if retailer_json.get('address'): 105 | self.assertTrue(retailer_json.get('address').get('city')) 106 | self.assertTrue(retailer_json.get('address').get('state')) 107 | self.assertTrue(retailer_json.get('address').get('country')) 108 | self.assertTrue(retailer_json.get('address').get('latitude')) 109 | self.assertTrue(retailer_json.get('address').get('longitude')) 110 | 111 | def test_get_retailer_invalid_input(self): 112 | """With invalid inputs, are correct errors thrown?""" 113 | 114 | self.assertRaises(ResourceDoesNotExistException, 115 | retailer_service.get_retailer, 116 | self.loopback_token, '123321') 117 | 118 | def test_get_retailer_invalid_token(self): 119 | """With an invalid token, are correct errors thrown?""" 120 | 121 | # Get retailers 122 | retailers = retailer_service.get_retailers(self.loopback_token) 123 | retailer_id = loads(retailers)[0].get('id') 124 | 125 | # Attempt to get a retailer with invalid token 126 | self.assertRaises(AuthenticationException, 127 | retailer_service.get_retailer, 128 | utils.get_bad_token(), retailer_id) 129 | 130 | def tearDown(self): 131 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 132 | 133 | 134 | class GetRetailerInventoryTestCase(unittest.TestCase): 135 | """Tests for `services/retailers.py - get_retailer_inventory()`.""" 136 | 137 | def setUp(self): 138 | # Create demo 139 | self.demo = demo_service.create_demo() 140 | demo_json = loads(self.demo) 141 | demo_guid = demo_json.get('guid') 142 | demo_user_id = demo_json.get('users')[0].get('id') 143 | 144 | # Log in user 145 | auth_data = user_service.login(demo_guid, demo_user_id) 146 | self.loopback_token = auth_data.get('loopback_token') 147 | 148 | def test_get_retailer_inventory_success(self): 149 | """With correct values, is valid inventory returned?""" 150 | 151 | # Get retailer 152 | retailers = retailer_service.get_retailers(self.loopback_token) 153 | retailer_id = loads(retailers)[0].get('id') 154 | inventory = retailer_service.get_retailer_inventory(self.loopback_token, retailer_id) 155 | 156 | # TODO: Update to use assertIsInstance(a,b) 157 | # Check all expected object values are present 158 | inventories_json = loads(inventory) 159 | for inventory_json in inventories_json: 160 | self.assertTrue(inventory_json.get('id')) 161 | self.assertIsInstance(inventory_json.get('quantity'), IntType) 162 | self.assertTrue(inventory_json.get('productId')) 163 | self.assertTrue(inventory_json.get('locationId')) 164 | self.assertTrue(inventory_json.get('locationType')) 165 | 166 | def test_get_retailer_inventory_invalid_input(self): 167 | """With invalid inputs, are correct errors thrown?""" 168 | 169 | self.assertRaises(ResourceDoesNotExistException, 170 | retailer_service.get_retailer_inventory, 171 | self.loopback_token, '123321') 172 | 173 | def test_get_retailer_inventory_invalid_token(self): 174 | """With an invalid token, are correct errors thrown?""" 175 | 176 | # Get retailers 177 | retailers = retailer_service.get_retailers(self.loopback_token) 178 | retailer_id = loads(retailers)[0].get('id') 179 | 180 | # Attempt to get retailer inventory with invalid token 181 | self.assertRaises(AuthenticationException, 182 | retailer_service.get_retailer_inventory, 183 | utils.get_bad_token(), retailer_id) 184 | 185 | def tearDown(self): 186 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 187 | 188 | if __name__ == '__main__': 189 | unittest.main() 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | **master** | [![Build Status](https://travis-ci.org/IBM/acme-freight-controller.svg?branch=master)](https://travis-ci.org/IBM/acme-freight-controller) [![Coverage Status](https://coveralls.io/repos/github/IBM/acme-freight-controller/badge.svg?branch=master)](https://coveralls.io/github/IBM/acme-freight-controller?branch=master) | 2 | | ----- | ----- | 3 | | **dev** | [![Build Status](https://travis-ci.org/IBM/acme-freight-controller.svg?branch=dev)](https://travis-ci.org/IBM/acme-freight-controller) [![Coverage Status](https://coveralls.io/repos/github/IBM/acme-freight-controller/badge.svg?branch=dev)](https://coveralls.io/github/IBM/acme-freight-controller?branch=dev)| 4 | 5 | # Acme Freight Controller 6 | 7 | This service is part of the larger [Acme Freight](https://github.com/ibm/acme-freight) project. 8 | 9 | ## Overview 10 | 11 | This service acts as the main controller for interaction between the system's services. 12 | 13 | To automatically deploy this application to Bluemix as part of the larger Acme Freight application, refer to the Bluemix DevOps toolchain on the [parent repository](https://github.com/ibm/acme-freight). 14 | 15 | 16 | ## Running the app on Bluemix 17 | 18 | 1. If you do not already have a Bluemix account, [sign up here][bluemix_signup_url] 19 | 20 | 1. Download and install the [Cloud Foundry CLI][cloud_foundry_url] tool 21 | 22 | 1. The app depends on the [ERP](https://github.com/ibm/acme-freight-erp) and [Recommendation](https://github.com/ibm/acme-freight-recommendation) microservices. These applications are deployed automatically as part of the toolchain on the parent `acme-freight` repository. 23 | 24 | 1. Clone the app to your local environment from your terminal using the following command: 25 | 26 | ```bash 27 | git clone https://github.com/ibm/acme-freight-controller.git 28 | ``` 29 | 30 | 1. `cd` into this newly created directory 31 | 32 | 1. Open the `manifest.yml` file and change the `host` value to something unique. 33 | 34 | The host you choose will determinate the subdomain of your application's URL: `.mybluemix.net` 35 | 36 | 1. Connect to Bluemix in the command line tool and follow the prompts to log in. 37 | 38 | ```bash 39 | cf api https://api.ng.bluemix.net 40 | cf login 41 | ``` 42 | 1. Push the app to Bluemix. 43 | 44 | ```bash 45 | cf push --no-start 46 | ``` 47 | 48 | 1. Define the environment variable pointing to the ERP service. 49 | 50 | ``` 51 | cf set-env acme-freight-controller ERP_SERVICE 52 | ``` 53 | 54 | 1. Define the OpenWhisk auth key and the package where the actions of the Recommendation service have been deployed 55 | 56 | ``` 57 | cf set-env acme-freight-controller OPENWHISK_AUTH "your-auth-key" 58 | cf set-env acme-freight-controller OPENWHISK_PACKAGE lwr 59 | ``` 60 | 61 | 1. Start the app. 62 | 63 | ```bash 64 | cf start acme-freight-controller 65 | ``` 66 | 67 | And voila! You now have your very own instance of the Acme Freight controller running on Bluemix. 68 | 69 | ## Run the app locally 70 | 71 | 1. If you have not already, [download Python 2.7][download_python_url] and install it on your local machine. 72 | 73 | 2. Clone the app to your local environment from your terminal using the following command: 74 | 75 | ```bash 76 | git clone https://github.com/ibm/acme-freight-controller.git 77 | ``` 78 | 79 | 3. `cd` into this newly created directory 80 | 81 | 4. In order to create an isolated development environment, we will be using Python's [virtualenv][virtualenv_url] tool. If you do not have it installed already, run 82 | 83 | ```bash 84 | pip install virtualenv 85 | ``` 86 | 87 | Then create a virtual environment called `venv` by running 88 | 89 | ```bash 90 | virtualenv venv 91 | ``` 92 | 93 | 5. Activate this new environment with 94 | 95 | ```bash 96 | source .env 97 | ``` 98 | 99 | 6. Install module requirements 100 | 101 | ```bash 102 | pip install -r requirements.dev.txt 103 | ``` 104 | 105 | 7. Finally, start the app 106 | 107 | ```bash 108 | python bin/start_web.py 109 | ``` 110 | 111 | To override values for your local environment variables create a file named `.env.local` from the template: 112 | 113 | ``` 114 | cp template-env.local .env.local 115 | ``` 116 | 117 | and edit the file to match your environment. 118 | 119 | ## Testing 120 | 121 | ### Unit Tests 122 | There are series of unit tests located in the [`server/tests`](server/tests) folder. The test suites are composed using the Python [unittest framework][unittest_docs_url]. To run the tests, execute the following command: 123 | 124 | ```bash 125 | python server/tests/run_unit_tests.py 126 | ``` 127 | 128 | ### Integration Tests 129 | Similar as the unit tests but they validate the communication between the controller 130 | and the other services, like the ERP service. These tests require a ERP service to be running. 131 | 132 | To run the tests, execute the following command: 133 | 134 | ```bash 135 | python server/tests/run_integration_tests.py 136 | ``` 137 | 138 | ### Travis CI 139 | One popular option for continuous integration is [Travis CI][travis_url]. We have provided a `.travis.yml` file in this repository for convenience. In order to set it up for your repository, take the following actions: 140 | 141 | 1. Go to your [Travis CI Profile][travis_profile_url] 142 | 143 | 2. Check the box next to your acme-freight GitHub repository and then click the settings cog 144 | 145 | 3. Create the following environment variables 146 | - `ACME_FREIGHT_ENV` - TEST 147 | 148 | Thats it! Now your future pushes to GitHub will be built and tested by Travis CI. 149 | 150 | ### Code Coverage Tests 151 | If you would like to perform code coverage tests as well, you can use [coveralls][coveralls_url] to perform this task. If you are using [Travis CI][travis_url] as your CI tool, simply replace `python` in your test commands with `coverage run` and then run `coveralls` as follows: 152 | 153 | ```bash 154 | $ coverage run server/tests/run_unit_tests.py 155 | $ coverage --append run server/tests/run_integration_tests.py 156 | $ coveralls 157 | ``` 158 | 159 | To determine how to run coveralls using another CI tool or for more in-depth instructions, check out the [coveralls usage documentation][coveralls_usage_url]. 160 | 161 | **Note**: To pass, the integration tests require an [ERP service][erp_github_url] to be running. 162 | 163 | 164 | ## API documentation 165 | The API methods that this component exposes requires the discovery of dependent services, however, the API will gracefully fail when they are not available. 166 | 167 | The API and data models are defined in [this Swagger 2.0 file](swagger.yaml). You can view this file in the [Swagger Editor](http://editor.swagger.io/#/?import=https://raw.githubusercontent.com/strongloop/acme-freight-controller/master/swagger.yaml). 168 | 169 | Use the Postman collection to help you get started with the controller API: 170 | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/b39a8c0ce27371fbd972#?env%5BLW_Prod%5D=W3sia2V5IjoiZXJwX2hvc3QiLCJ2YWx1ZSI6Imh0dHA6Ly9sb2dpc3RpY3Mtd2l6YXJkLWVycC5teWJsdWVtaXgubmV0LyIsInR5cGUiOiJ0ZXh0IiwiZW5hYmxlZCI6dHJ1ZSwiaG92ZXJlZCI6ZmFsc2V9LHsia2V5IjoiY29udHJvbGxlcl9ob3N0IiwidmFsdWUiOiJodHRwczovL2xvZ2lzdGljcy13aXphcmQubXlibHVlbWl4Lm5ldCIsInR5cGUiOiJ0ZXh0IiwiZW5hYmxlZCI6dHJ1ZSwiaG92ZXJlZCI6ZmFsc2V9XQ==) 171 | 172 | ## Troubleshooting 173 | 174 | The primary source of debugging information for your Bluemix app is the logs. To see them, run the following command using the Cloud Foundry CLI: 175 | 176 | ``` 177 | $ cf logs acme-freight-controller --recent 178 | ``` 179 | For more detailed information on troubleshooting your application, see the [Troubleshooting section](https://www.ng.bluemix.net/docs/troubleshoot/tr.html) in the Bluemix documentation. 180 | 181 | ## Privacy Notice 182 | 183 | The acme-freight sample web application includes code to track deployments to Bluemix and other Cloud Foundry platforms. The following information is sent to a [Deployment Tracker](https://github.com/ibm/cf-deployment-tracker-service) service on each deployment: 184 | 185 | * Python package version 186 | * Python repository URL 187 | * Application Name (`application_name`) 188 | * Space ID (`space_id`) 189 | * Application Version (`application_version`) 190 | * Application URIs (`application_uris`) 191 | * Labels of bound services 192 | * Number of instances for each bound service and associated plan information 193 | 194 | This data is collected from the `setup.py` file in the sample application and the `VCAP_APPLICATION` and `VCAP_SERVICES` environment variables in IBM Bluemix and other Cloud Foundry platforms. This data is used by IBM to track metrics around deployments of sample applications to IBM Bluemix to measure the usefulness of our examples, so that we can continuously improve the content we offer to you. Only deployments of sample applications that include code to ping the Deployment Tracker service will be tracked. 195 | 196 | ### Disabling Deployment Tracking 197 | 198 | Deployment tracking can be disabled by removing `cf_deployment_tracker.track()` from the `server/web/__init__.py` file. 199 | 200 | ## License 201 | 202 | See [LICENSE](LICENSE) for license information. 203 | 204 | 205 | [erp_github_url]: https://github.com/ibm/acme-freight-erp 206 | [recommendation_github_url]: https://github.com/ibm/acme-freight-recommendation 207 | [toolchain_github_url]: https://github.com/ibm/acme-freight-toolchain 208 | [bluemix_signup_url]: http://ibm.biz/logistics-wizard-signup 209 | [cloud_foundry_url]: https://github.com/cloudfoundry/cli 210 | [download_python_url]: https://www.python.org/downloads/ 211 | [virtualenv_url]: http://docs.python-guide.org/en/latest/dev/virtualenvs/ 212 | [unittest_docs_url]: https://docs.python.org/3/library/unittest.html 213 | [travis_url]: https://travis-ci.org/ 214 | [travis_profile_url]: https://travis-ci.org/profile/ 215 | [coveralls_url]: https://coveralls.io/ 216 | [coveralls_usage_url]: https://pypi.python.org/pypi/coveralls#usage-travis-ci 217 | -------------------------------------------------------------------------------- /server/tests/test_distribution_centers_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads 3 | from types import IntType 4 | import server.tests.utils as utils 5 | import server.services.demos as demo_service 6 | import server.services.users as user_service 7 | import server.services.distribution_centers as distribution_center_service 8 | from server.exceptions import (AuthenticationException, 9 | ResourceDoesNotExistException) 10 | 11 | 12 | def suite(): 13 | test_suite = unittest.TestSuite() 14 | test_suite.addTest(GetDistributionCentersTestCase('test_get_distribution_centers_success')) 15 | test_suite.addTest(GetDistributionCentersTestCase('test_get_distribution_centers_invalid_token')) 16 | test_suite.addTest(GetDistributionCenterTestCase('test_get_distribution_center_success')) 17 | test_suite.addTest(GetDistributionCenterTestCase('test_get_distribution_center_invalid_input')) 18 | test_suite.addTest(GetDistributionCenterTestCase('test_get_distribution_center_invalid_token')) 19 | test_suite.addTest(GetDistributionCenterInventoryTestCase('test_get_distribution_center_inventory_success')) 20 | test_suite.addTest(GetDistributionCenterInventoryTestCase('test_get_distribution_center_inventory_invalid_input')) 21 | test_suite.addTest(GetDistributionCenterInventoryTestCase('test_get_distribution_center_inventory_invalid_token')) 22 | return test_suite 23 | 24 | 25 | ########################### 26 | # Unit Tests # 27 | ########################### 28 | 29 | class GetDistributionCentersTestCase(unittest.TestCase): 30 | """Tests for `services/distribution_centers.py - get_distribution_centers()`.""" 31 | 32 | def setUp(self): 33 | # Create demo 34 | self.demo = demo_service.create_demo() 35 | demo_json = loads(self.demo) 36 | demo_guid = demo_json.get('guid') 37 | demo_user_id = demo_json.get('users')[0].get('id') 38 | 39 | # Log in user 40 | auth_data = user_service.login(demo_guid, demo_user_id) 41 | self.loopback_token = auth_data.get('loopback_token') 42 | 43 | def test_get_distribution_centers_success(self): 44 | """With correct values, are valid distribution centers returned?""" 45 | 46 | # Get distribution centers 47 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 48 | 49 | # TODO: Update to use assertIsInstance(a,b) 50 | # Check all expected object values are present 51 | distribution_centers_json = loads(distribution_centers) 52 | # Check that the distribution centers are valid 53 | for distribution_center_json in distribution_centers_json: 54 | self.assertTrue(distribution_center_json.get('id')) 55 | 56 | # Check that distribution center address is valid, if present 57 | if distribution_center_json.get('address'): 58 | self.assertTrue(distribution_center_json.get('address').get('city')) 59 | self.assertTrue(distribution_center_json.get('address').get('state')) 60 | self.assertTrue(distribution_center_json.get('address').get('country')) 61 | self.assertTrue(distribution_center_json.get('address').get('latitude')) 62 | self.assertTrue(distribution_center_json.get('address').get('longitude')) 63 | 64 | # Check that distribution center contact is valid, if present 65 | if distribution_center_json.get('contact'): 66 | self.assertTrue(distribution_center_json.get('contact').get('name')) 67 | 68 | def test_get_distribution_centers_invalid_token(self): 69 | """With an invalid token, are correct errors thrown?""" 70 | 71 | self.assertRaises(AuthenticationException, 72 | distribution_center_service.get_distribution_centers, 73 | utils.get_bad_token()) 74 | 75 | def tearDown(self): 76 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 77 | 78 | 79 | class GetDistributionCenterTestCase(unittest.TestCase): 80 | """Tests for `services/distribution_centers.py - get_distribution_center()`.""" 81 | 82 | def setUp(self): 83 | # Create demo 84 | self.demo = demo_service.create_demo() 85 | demo_json = loads(self.demo) 86 | demo_guid = demo_json.get('guid') 87 | demo_user_id = demo_json.get('users')[0].get('id') 88 | 89 | # Log in user 90 | auth_data = user_service.login(demo_guid, demo_user_id) 91 | self.loopback_token = auth_data.get('loopback_token') 92 | 93 | def test_get_distribution_center_success(self): 94 | """With correct values, is a valid distribution center returned?""" 95 | 96 | # Get distribution center 97 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 98 | dc_id = loads(distribution_centers)[0].get('id') 99 | distribution_center = distribution_center_service.get_distribution_center(self.loopback_token, dc_id) 100 | 101 | # TODO: Update to use assertIsInstance(a,b) 102 | # Check all expected object values are present 103 | distribution_center_json = loads(distribution_center) 104 | # Check that the distribution center is valid 105 | self.assertTrue(distribution_center_json.get('id')) 106 | 107 | # Check that distribution center address is valid, if present 108 | if distribution_center_json.get('address'): 109 | self.assertTrue(distribution_center_json.get('address').get('city')) 110 | self.assertTrue(distribution_center_json.get('address').get('state')) 111 | self.assertTrue(distribution_center_json.get('address').get('country')) 112 | self.assertTrue(distribution_center_json.get('address').get('latitude')) 113 | self.assertTrue(distribution_center_json.get('address').get('longitude')) 114 | 115 | # Check that distribution center contact is valid, if present 116 | if distribution_center_json.get('contact'): 117 | self.assertTrue(distribution_center_json.get('contact').get('name')) 118 | 119 | def test_get_distribution_center_invalid_input(self): 120 | """With invalid inputs, are correct errors thrown?""" 121 | 122 | self.assertRaises(ResourceDoesNotExistException, 123 | distribution_center_service.get_distribution_center, 124 | self.loopback_token, '123321') 125 | 126 | def test_get_distribution_center_invalid_token(self): 127 | """With an invalid token, are correct errors thrown?""" 128 | 129 | # Get distribution centers 130 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 131 | dc_id = loads(distribution_centers)[0].get('id') 132 | 133 | # Attempt to get a distribution center with invalid token 134 | self.assertRaises(AuthenticationException, 135 | distribution_center_service.get_distribution_center, 136 | utils.get_bad_token(), dc_id) 137 | 138 | def tearDown(self): 139 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 140 | 141 | 142 | class GetDistributionCenterInventoryTestCase(unittest.TestCase): 143 | """Tests for `services/distribution_centers.py - get_distribution_center_inventory()`.""" 144 | 145 | def setUp(self): 146 | # Create demo 147 | self.demo = demo_service.create_demo() 148 | demo_json = loads(self.demo) 149 | demo_guid = demo_json.get('guid') 150 | demo_user_id = demo_json.get('users')[0].get('id') 151 | 152 | # Log in user 153 | auth_data = user_service.login(demo_guid, demo_user_id) 154 | self.loopback_token = auth_data.get('loopback_token') 155 | 156 | def test_get_distribution_center_inventory_success(self): 157 | """With correct values, is valid inventory returned?""" 158 | 159 | # Get distribution center 160 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 161 | dc_id = loads(distribution_centers)[0].get('id') 162 | inventory = distribution_center_service.get_distribution_center_inventory(self.loopback_token, dc_id) 163 | 164 | # TODO: Update to use assertIsInstance(a,b) 165 | # Check all expected object values are present 166 | inventories_json = loads(inventory) 167 | for inventory_json in inventories_json: 168 | self.assertTrue(inventory_json.get('id')) 169 | self.assertIsInstance(inventory_json.get('quantity'), IntType) 170 | self.assertTrue(inventory_json.get('productId')) 171 | self.assertTrue(inventory_json.get('locationId')) 172 | self.assertTrue(inventory_json.get('locationType')) 173 | 174 | def test_get_distribution_center_inventory_invalid_input(self): 175 | """With invalid inputs, are correct errors thrown?""" 176 | 177 | self.assertRaises(ResourceDoesNotExistException, 178 | distribution_center_service.get_distribution_center_inventory, 179 | self.loopback_token, '123321') 180 | 181 | def test_get_distribution_center_inventory_invalid_token(self): 182 | """With an invalid token, are correct errors thrown?""" 183 | 184 | # Get distribution centers 185 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 186 | dc_id = loads(distribution_centers)[0].get('id') 187 | 188 | # Attempt to get retailer inventory with invalid token 189 | self.assertRaises(AuthenticationException, 190 | distribution_center_service.get_distribution_center_inventory, 191 | utils.get_bad_token(), dc_id) 192 | 193 | def tearDown(self): 194 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 195 | 196 | if __name__ == '__main__': 197 | unittest.main() 198 | -------------------------------------------------------------------------------- /server/tests/test_demos_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | from json import loads 4 | from multiprocessing import Pool 5 | import server.tests.utils as utils 6 | import server.services.demos as demo_service 7 | import server.services.users as user_service 8 | import server.services.shipments as shipment_service 9 | import server.services.distribution_centers as distribution_center_service 10 | import server.services.retailers as retailer_service 11 | from server.utils import async_helper 12 | from server.exceptions import (UnprocessableEntityException, 13 | ResourceDoesNotExistException) 14 | 15 | 16 | def suite(): 17 | test_suite = unittest.TestSuite() 18 | test_suite.addTest(CreateDemoTestCase('test_demo_create_success')) 19 | test_suite.addTest(RetrieveDemoTestCase('test_demo_retrieve_success')) 20 | test_suite.addTest(RetrieveDemoTestCase('test_demo_retrieve_invalid_input')) 21 | test_suite.addTest(RetrieveDemoTestCase('test_demo_retrieve_retailers_success')) 22 | test_suite.addTest(RetrieveDemoTestCase('test_admin_data_async_success')) 23 | test_suite.addTest(DeleteDemoTestCase('test_demo_delete_success')) 24 | test_suite.addTest(DeleteDemoTestCase('test_demo_delete_invalid_input')) 25 | return test_suite 26 | 27 | 28 | ########################### 29 | # Unit Tests # 30 | ########################### 31 | 32 | class CreateDemoTestCase(unittest.TestCase): 33 | """Tests for `services/demos.py - create_demo()`.""" 34 | 35 | def test_demo_create_success(self): 36 | """With correct values, is a valid demo returned?""" 37 | 38 | # Create demo 39 | demo = demo_service.create_demo() 40 | 41 | # TODO: Update to use assertIsInstance(a,b) 42 | # Check all expected object values are present 43 | demo_json = loads(demo) 44 | self.assertTrue(demo_json.get('id')) 45 | self.assertTrue(demo_json.get('guid')) 46 | self.assertTrue(demo_json.get('createdAt')) 47 | self.assertTrue(demo_json.get('users')) 48 | 49 | # Check that the default supplychainmanager user was created 50 | created_user_json = demo_json.get('users')[0] 51 | self.assertTrue(created_user_json.get('id')) 52 | self.assertTrue(created_user_json.get('demoId')) 53 | self.assertTrue(created_user_json.get('username')) 54 | self.assertTrue(created_user_json.get('email')) 55 | self.assertTrue(created_user_json.get('roles')) 56 | 57 | # Check that the proper role was created 58 | scm_role_json = created_user_json.get('roles')[0] 59 | self.assertTrue(scm_role_json.get('id')) 60 | self.assertTrue(scm_role_json.get('name') == "supplychainmanager") 61 | self.assertTrue(scm_role_json.get('created')) 62 | self.assertTrue(scm_role_json.get('modified')) 63 | 64 | # Destroy demo 65 | demo_service.delete_demo_by_guid(demo_json.get('guid')) 66 | 67 | 68 | class RetrieveDemoTestCase(unittest.TestCase): 69 | """Tests for `services/demos.py - get_demo_by_guid(), get_demo_retailers()`. 70 | Tests for `web/utils.py - async_helper()`.""" 71 | 72 | def setUp(self): 73 | # Create demo 74 | self.demo = demo_service.create_demo() 75 | demo_json = loads(self.demo) 76 | demo_guid = demo_json.get('guid') 77 | demo_user_id = demo_json.get('users')[0].get('id') 78 | 79 | # Log in user 80 | auth_data = user_service.login(demo_guid, demo_user_id) 81 | self.loopback_token = auth_data.get('loopback_token') 82 | 83 | def test_demo_retrieve_success(self): 84 | """With correct values, is a valid demo returned?""" 85 | 86 | # Retrieve demo 87 | retrieved_demo = demo_service.get_demo_by_guid(loads(self.demo).get('guid')) 88 | 89 | # TODO: Update to use assertIsInstance(a,b) 90 | # Check all expected object values are present 91 | created_demo_json = loads(self.demo) 92 | demo_json = loads(retrieved_demo) 93 | self.assertTrue(demo_json.get('id') == created_demo_json.get('id')) 94 | self.assertTrue(demo_json.get('guid') == created_demo_json.get('guid')) 95 | self.assertTrue(demo_json.get('name') == created_demo_json.get('name')) 96 | self.assertTrue(demo_json.get('createdAt') == created_demo_json.get('createdAt')) 97 | self.assertTrue(demo_json.get('users')) 98 | 99 | # Check that the users are valid 100 | for user_json in demo_json.get('users'): 101 | self.assertTrue(user_json.get('id')) 102 | self.assertTrue(user_json.get('demoId')) 103 | self.assertTrue(user_json.get('username')) 104 | self.assertTrue(user_json.get('email')) 105 | 106 | # Check that user roles are valid, if present 107 | if user_json.get('roles'): 108 | for role_json in user_json.get('roles'): 109 | self.assertTrue(role_json.get('id')) 110 | self.assertTrue(role_json.get('name')) 111 | self.assertTrue(role_json.get('created')) 112 | self.assertTrue(role_json.get('modified')) 113 | 114 | def test_demo_retrieve_invalid_input(self): 115 | """With invalid guid, is correct error thrown?""" 116 | self.assertRaises(ResourceDoesNotExistException, 117 | demo_service.get_demo_by_guid, 118 | 'ABC123') 119 | 120 | def test_demo_retrieve_retailers_success(self): 121 | """With correct values, are valid demo retailers returned?""" 122 | 123 | # Retrieve demo retailers 124 | demo_guid = loads(self.demo).get('guid') 125 | retailers = demo_service.get_demo_retailers(demo_guid) 126 | retailers_json = loads(retailers) 127 | 128 | # TODO: Update to use assertIsInstance(a,b) 129 | # Check that the retailers are valid 130 | for retailer_json in retailers_json: 131 | self.assertTrue(retailer_json.get('id')) 132 | self.assertTrue(retailer_json.get('address')) 133 | 134 | address_json = retailer_json.get('address') 135 | self.assertTrue(address_json.get('city')) 136 | self.assertTrue(address_json.get('state')) 137 | self.assertTrue(address_json.get('country')) 138 | self.assertTrue(address_json.get('latitude')) 139 | self.assertTrue(address_json.get('longitude')) 140 | 141 | def test_admin_data_async_success(self): 142 | """With correct values, is valid data returned asynchronously?""" 143 | 144 | # Specify functions and their corresponding arguments to be called 145 | erp_calls = [(shipment_service.get_shipments, self.loopback_token), 146 | (distribution_center_service.get_distribution_centers, self.loopback_token), 147 | (retailer_service.get_retailers, self.loopback_token)] 148 | pool = Pool(processes=len(erp_calls)) 149 | 150 | # Asynchronously make calls and then wait on all processes to finish 151 | results = pool.map(async_helper, erp_calls) 152 | pool.close() 153 | pool.join() 154 | 155 | # Check that the shipment is valid 156 | shipment = loads(results[0])[0] 157 | self.assertTrue(shipment.get('id')) 158 | self.assertTrue(shipment.get('status')) 159 | self.assertTrue(shipment.get('createdAt')) 160 | self.assertTrue(shipment.get('estimatedTimeOfArrival')) 161 | self.assertTrue(shipment.get('fromId')) 162 | self.assertTrue(shipment.get('toId')) 163 | if shipment.get('currentLocation'): 164 | self.assertTrue(shipment.get('currentLocation').get('city')) 165 | self.assertTrue(shipment.get('currentLocation').get('state')) 166 | self.assertTrue(shipment.get('currentLocation').get('country')) 167 | self.assertTrue(shipment.get('currentLocation').get('latitude')) 168 | self.assertTrue(shipment.get('currentLocation').get('longitude')) 169 | 170 | # Check that the retailer is valid 171 | retailer = loads(results[1])[0] 172 | self.assertTrue(retailer.get('id')) 173 | if retailer.get('address'): 174 | self.assertTrue(retailer.get('address').get('city')) 175 | self.assertTrue(retailer.get('address').get('state')) 176 | self.assertTrue(retailer.get('address').get('country')) 177 | self.assertTrue(retailer.get('address').get('latitude')) 178 | self.assertTrue(retailer.get('address').get('longitude')) 179 | 180 | # Check that the distribution center is valid 181 | distribution_center = loads(results[2])[0] 182 | self.assertTrue(distribution_center.get('id')) 183 | if distribution_center.get('address'): 184 | self.assertTrue(distribution_center.get('address').get('city')) 185 | self.assertTrue(distribution_center.get('address').get('state')) 186 | self.assertTrue(distribution_center.get('address').get('country')) 187 | self.assertTrue(distribution_center.get('address').get('latitude')) 188 | self.assertTrue(distribution_center.get('address').get('longitude')) 189 | 190 | def tearDown(self): 191 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 192 | 193 | 194 | class DeleteDemoTestCase(unittest.TestCase): 195 | """Tests for `services/demos.py - delete_demo_by_guid()`.""" 196 | 197 | def setUp(self): 198 | # Create demo 199 | self.demo = demo_service.create_demo() 200 | 201 | def test_demo_delete_success(self): 202 | """With correct values, is a valid demo deleted?""" 203 | 204 | self.assertTrue(demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) is None) 205 | 206 | def test_demo_delete_invalid_input(self): 207 | """With invalid guid, is correct error thrown?""" 208 | 209 | # Attempt to delete demo with invalid guid 210 | self.assertRaises(ResourceDoesNotExistException, 211 | demo_service.delete_demo_by_guid, 212 | 'ABC123') 213 | 214 | 215 | if __name__ == '__main__': 216 | unittest.main() 217 | -------------------------------------------------------------------------------- /server/services/shipments.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle all actions on the shipment resource and is responsible for making sure 3 | the calls get routed to the ERP service appropriately. As much as possible, 4 | the interface layer should have no knowledge of the properties of the shipment 5 | object and should just call into the service layer to act upon a shipment resource. 6 | """ 7 | import requests 8 | import json 9 | from server.utils import get_service_url 10 | from server.utils import get_apic_credentials 11 | from server.exceptions import (APIException, 12 | AuthenticationException, 13 | UnprocessableEntityException, 14 | ResourceDoesNotExistException, 15 | ValidationException) 16 | 17 | ########################### 18 | # Utilities # 19 | ########################### 20 | 21 | 22 | def shipment_to_dict(shipment): 23 | """ 24 | Convert an instance of the Shipment model to a dict. 25 | 26 | :param shipment: An instance of the Shipment model. 27 | :return: A dict representing the shipment. 28 | """ 29 | return { 30 | 'id': shipment.id, 31 | 'status': shipment.status, 32 | 'createdAt': shipment.createdAt, 33 | 'updatedAt': shipment.updatedAt, 34 | 'deliveredAt': shipment.deliveredAt, 35 | 'estimatedTimeOfArrival': shipment.estimatedTimeOfArrival, 36 | 'currentLocation': shipment.currentLocation, 37 | 'fromId': shipment.fromId, 38 | 'toId': shipment.toId 39 | } 40 | 41 | 42 | def add_query_filter(cur_query, filter_type, op, value, property_name=None): 43 | """ 44 | Add a query condition to an input query string 45 | 46 | :param cur_query: The current query string. 47 | :param filter_type: The type of Loopback filter for the query. 48 | :param property_name: The object's property used by the query to filter the result set. 49 | :param op: Equivalence operator for the query's evaluation. 50 | :param value: The value that the property is evaluated against. 51 | 52 | :return: The list of existing shipments. 53 | """ 54 | 55 | # If the query string is null, initialize it 56 | # If it is non-empty, separate from new query with ampersand 57 | if cur_query is None or cur_query == "": 58 | cur_query = "?" 59 | else: 60 | cur_query += "&" 61 | 62 | cur_query += "filter[" + filter_type + "]" 63 | if property_name is not None: 64 | cur_query += "[" + property_name + "]" 65 | return cur_query + op + str(value) 66 | 67 | 68 | ########################### 69 | # Services # 70 | ########################### 71 | 72 | def get_shipments(token, retailer_id=None, dc_id=None, status=None): 73 | """ 74 | Get a list of shipments from the ERP system. 75 | 76 | :param token: The ERP Loopback session token. 77 | :param status: Status of the shipments to be retrieved. 78 | :param retailer_id: Retailer of the shipments to be retrieved. 79 | :param dc_id: Distribution center of the shipments to be retrieved. 80 | 81 | :return: The list of existing shipments. 82 | """ 83 | 84 | # Add filters if corresponding inputs are present 85 | status_query = "" 86 | if status is not None: 87 | status_query = add_query_filter(status_query, "where", "=", status, property_name="status") 88 | if retailer_id is not None: 89 | status_query = add_query_filter(status_query, "where", "=", retailer_id, property_name="toId") 90 | if dc_id is not None: 91 | status_query = add_query_filter(status_query, "where", "=", dc_id, property_name="fromId") 92 | 93 | # Create and format request to ERP 94 | url = '%s/api/v1/Shipments%s' % (get_service_url('lw-erp'), status_query) 95 | headers = { 96 | 'cache-control': "no-cache", 97 | 'Authorization': token 98 | } 99 | headers.update(get_apic_credentials()) 100 | 101 | try: 102 | response = requests.request("GET", url, headers=headers) 103 | except Exception as e: 104 | raise APIException('ERP threw error retrieving shipments', internal_details=str(e)) 105 | 106 | # Check for possible errors in response 107 | if response.status_code == 401: 108 | raise AuthenticationException('ERP access denied', 109 | internal_details=json.loads(response.text).get('error').get('message')) 110 | 111 | return response.text 112 | 113 | 114 | def get_shipment(token, shipment_id, include_items=None): 115 | """ 116 | Get a shipment from the ERP system. 117 | 118 | :param token: The ERP Loopback session token. 119 | :param shipment_id: The ID of the shipment to be retrieved. 120 | :param include_items: Indicates if items are to be returned with shipment. 121 | 122 | :return: The retrieved shipment. 123 | """ 124 | 125 | # Add filters if corresponding inputs are present 126 | status_query = "" 127 | if include_items != "0": 128 | status_query = add_query_filter(status_query, "include", "=", "items") 129 | 130 | # Create and format request to ERP 131 | url = '%s/api/v1/Shipments/%s%s' % (get_service_url('lw-erp'), str(shipment_id), status_query) 132 | headers = { 133 | 'cache-control': "no-cache", 134 | 'Authorization': token 135 | } 136 | headers.update(get_apic_credentials()) 137 | 138 | try: 139 | response = requests.request("GET", url, headers=headers) 140 | except Exception as e: 141 | raise APIException('ERP threw error retrieving shipment', internal_details=str(e)) 142 | 143 | # Check for possible errors in response 144 | if response.status_code == 401: 145 | raise AuthenticationException('ERP access denied', 146 | internal_details=json.loads(response.text).get('error').get('message')) 147 | elif response.status_code == 404: 148 | raise ResourceDoesNotExistException('Shipment does not exist', 149 | internal_details=json.loads(response.text).get('error').get('message')) 150 | 151 | return response.text 152 | 153 | 154 | def create_shipment(token, shipment): 155 | """ 156 | Create a shipment in the ERP system. 157 | 158 | :param token: The ERP Loopback session token. 159 | :param shipment: The shipment object to be created. 160 | 161 | :return: The created shipment. 162 | """ 163 | 164 | # Create and format request to ERP 165 | url = '%s/api/v1/Shipments' % get_service_url('lw-erp') 166 | headers = { 167 | 'content-type': "application/json", 168 | 'cache-control': "no-cache", 169 | 'Authorization': token 170 | } 171 | headers.update(get_apic_credentials()) 172 | 173 | shipment_json = json.dumps(shipment) 174 | 175 | try: 176 | response = requests.request("POST", url, data=shipment_json, headers=headers) 177 | except Exception as e: 178 | raise APIException('ERP threw error creating shipment', internal_details=str(e)) 179 | 180 | # Check for possible errors in response 181 | if response.status_code == 400: 182 | raise ValidationException('Bad shipment data', 183 | internal_details=json.loads(response.text).get('error').get('message')) 184 | elif response.status_code == 401: 185 | raise AuthenticationException('ERP access denied', 186 | internal_details=json.loads(response.text).get('error').get('message')) 187 | elif response.status_code == 422: 188 | raise UnprocessableEntityException('Required data for shipment is either absent or invalid', 189 | internal_details=json.loads(response.text).get('error').get('message')) 190 | 191 | return response.text 192 | 193 | 194 | def delete_shipment(token, shipment_id): 195 | """ 196 | Delete a shipment from the ERP system. 197 | 198 | :param token: The ERP Loopback session token. 199 | :param shipment_id: The ID of the shipment to be deleted. 200 | """ 201 | 202 | # Create and format request to ERP 203 | url = '%s/api/v1/Shipments/%s' % (get_service_url('lw-erp'), str(shipment_id)) 204 | headers = { 205 | 'cache-control': "no-cache", 206 | 'Authorization': token 207 | } 208 | headers.update(get_apic_credentials()) 209 | 210 | try: 211 | response = requests.request("DELETE", url, headers=headers) 212 | except Exception as e: 213 | raise APIException('ERP threw error deleting shipment', internal_details=str(e)) 214 | 215 | # Check for possible errors in response 216 | if response.status_code == 401: 217 | raise AuthenticationException('ERP access denied', 218 | internal_details=json.loads(response.text).get('error').get('message')) 219 | elif response.status_code == 404: 220 | raise ResourceDoesNotExistException('Shipment does not exist', 221 | internal_details=json.loads(response.text).get('error').get('message')) 222 | 223 | return 224 | 225 | 226 | def update_shipment(token, shipment_id, shipment): 227 | """ 228 | Update a shipment from the ERP system. 229 | 230 | :param token: The ERP Loopback session token. 231 | :param shipment_id: The ID of the shipment to be retrieved. 232 | :param shipment: The shipment object with values to update. 233 | 234 | :return: The updated shipment. 235 | """ 236 | 237 | # Create and format request to ERP 238 | url = '%s/api/v1/Shipments/%s' % (get_service_url('lw-erp'), str(shipment_id)) 239 | headers = { 240 | 'cache-control': "no-cache", 241 | 'Authorization': token 242 | } 243 | headers.update(get_apic_credentials()) 244 | 245 | try: 246 | response = requests.request("PUT", url, data=shipment, headers=headers) 247 | except Exception as e: 248 | raise APIException('ERP threw error updating shipment', internal_details=str(e)) 249 | 250 | # Check for possible errors in response 251 | if response.status_code == 400: 252 | raise ValidationException('Invalid update to shipment', 253 | internal_details=json.loads(response.text).get('error').get('message')) 254 | elif response.status_code == 401: 255 | raise AuthenticationException('ERP access denied', 256 | internal_details=json.loads(response.text).get('error').get('message')) 257 | elif response.status_code == 404: 258 | raise ResourceDoesNotExistException('Shipment does not exist', 259 | internal_details=json.loads(response.text).get('error').get('message')) 260 | 261 | return response.text 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /server/tests/test_shipments_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads, dumps 3 | import server.tests.utils as utils 4 | import server.services.demos as demo_service 5 | import server.services.users as user_service 6 | import server.services.shipments as shipment_service 7 | import server.services.retailers as retailer_service 8 | import server.services.distribution_centers as distribution_center_service 9 | from server.exceptions import (AuthenticationException, 10 | ResourceDoesNotExistException, 11 | UnprocessableEntityException, 12 | ValidationException) 13 | 14 | 15 | def suite(): 16 | test_suite = unittest.TestSuite() 17 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_success')) 18 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_status_filter_success')) 19 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_retailer_id_filter_success')) 20 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_distribution_center_id_filter_success')) 21 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_multiple_filters_success')) 22 | test_suite.addTest(GetShipmentsTestCase('test_get_shipments_invalid_token')) 23 | test_suite.addTest(CreateShipmentTestCase('test_create_shipment_success')) 24 | test_suite.addTest(CreateShipmentTestCase('test_create_shipment_invalid_ids')) 25 | test_suite.addTest(CreateShipmentTestCase('test_create_shipment_invalid_token')) 26 | test_suite.addTest(GetShipmentTestCase('test_get_shipment_success')) 27 | test_suite.addTest(GetShipmentTestCase('test_get_shipment_no_items_filter_success')) 28 | test_suite.addTest(GetShipmentTestCase('test_get_shipment_invalid_input')) 29 | test_suite.addTest(GetShipmentTestCase('test_get_shipment_invalid_token')) 30 | test_suite.addTest(DeleteShipmentTestCase('test_delete_shipment_success')) 31 | test_suite.addTest(DeleteShipmentTestCase('test_delete_shipment_invalid_input')) 32 | test_suite.addTest(DeleteShipmentTestCase('test_delete_shipment_invalid_token')) 33 | test_suite.addTest(UpdateShipmentTestCase('test_update_shipment_success')) 34 | test_suite.addTest(UpdateShipmentTestCase('test_update_invalid_status')) 35 | test_suite.addTest(UpdateShipmentTestCase('test_update_shipment_invalid_input')) 36 | test_suite.addTest(UpdateShipmentTestCase('test_update_shipment_invalid_token')) 37 | return test_suite 38 | 39 | # List of potential status values 40 | statuses = ['NEW', 'APPROVED', 'IN_TRANSIT', 'DELIVERED'] 41 | 42 | ########################### 43 | # Unit Tests # 44 | ########################### 45 | 46 | 47 | class GetShipmentsTestCase(unittest.TestCase): 48 | """Tests for `services/shipments.py - get_shipments()`.""" 49 | 50 | def setUp(self): 51 | # Create demo 52 | self.demo = demo_service.create_demo() 53 | demo_json = loads(self.demo) 54 | demo_guid = demo_json.get('guid') 55 | demo_user_id = demo_json.get('users')[0].get('id') 56 | 57 | # Log in user 58 | auth_data = user_service.login(demo_guid, demo_user_id) 59 | self.loopback_token = auth_data.get('loopback_token') 60 | 61 | def test_get_shipments_success(self): 62 | """With correct values, are valid shipments returned?""" 63 | 64 | # Get shipments 65 | shipments = shipment_service.get_shipments(self.loopback_token) 66 | 67 | # TODO: Update to use assertIsInstance(a,b) 68 | # Check all expected object values are present 69 | shipments_json = loads(shipments) 70 | # Check that the shipments are valid 71 | for shipment_json in shipments_json: 72 | self.assertTrue(shipment_json.get('id')) 73 | self.assertTrue(shipment_json.get('status')) 74 | self.assertTrue(shipment_json.get('createdAt')) 75 | self.assertTrue(shipment_json.get('estimatedTimeOfArrival')) 76 | self.assertTrue(shipment_json.get('fromId')) 77 | self.assertTrue(shipment_json.get('toId')) 78 | 79 | # Check that shipment address is valid, if present 80 | if shipment_json.get('currentLocation'): 81 | self.assertTrue(shipment_json.get('currentLocation').get('city')) 82 | self.assertTrue(shipment_json.get('currentLocation').get('state')) 83 | self.assertTrue(shipment_json.get('currentLocation').get('country')) 84 | self.assertTrue(shipment_json.get('currentLocation').get('latitude')) 85 | self.assertTrue(shipment_json.get('currentLocation').get('longitude')) 86 | 87 | def test_get_shipments_status_filter_success(self): 88 | """Are correct status shipments returned?""" 89 | 90 | # Get shipments with specific status 91 | query_status = 'DELIVERED' 92 | shipments = shipment_service.get_shipments(self.loopback_token, status=query_status) 93 | 94 | # TODO: Update to use assertIsInstance(a,b) 95 | # Check all expected object values are present 96 | shipments_json = loads(shipments) 97 | # Check that the shipments have correct status 98 | for shipment_json in shipments_json: 99 | self.assertTrue(shipment_json.get('status') == query_status) 100 | 101 | def test_get_shipments_retailer_id_filter_success(self): 102 | """Are correct retailers' shipments returned?""" 103 | 104 | # Get shipments intended for specific retailer 105 | retailers = retailer_service.get_retailers(self.loopback_token) 106 | retailer_id_filter = loads(retailers)[0].get('id') 107 | shipments = shipment_service.get_shipments(self.loopback_token, retailer_id=retailer_id_filter) 108 | 109 | # TODO: Update to use assertIsInstance(a,b) 110 | # Check all expected object values are present 111 | shipments_json = loads(shipments) 112 | # Check that the shipments have correct retailer ID (toId) 113 | for shipment_json in shipments_json: 114 | self.assertTrue(shipment_json.get('toId') == retailer_id_filter) 115 | 116 | def test_get_shipments_distribution_center_id_filter_success(self): 117 | """Are correct distribution center's shipments returned?""" 118 | 119 | # Get shipments intended for specific distribution center 120 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 121 | dc_id_filter = loads(distribution_centers)[0].get('id') 122 | shipments = shipment_service.get_shipments(self.loopback_token, dc_id=dc_id_filter) 123 | 124 | # TODO: Update to use assertIsInstance(a,b) 125 | # Check all expected object values are present 126 | shipments_json = loads(shipments) 127 | # Check that the shipments have correct retailer ID (toId) 128 | for shipment_json in shipments_json: 129 | self.assertTrue(shipment_json.get('fromId') == dc_id_filter) 130 | 131 | def test_get_shipments_multiple_filters_success(self): 132 | """Are correct shipments returned when using multiple filters?""" 133 | 134 | # Get filter values applicable to at least one shipment 135 | shipments = shipment_service.get_shipments(self.loopback_token) 136 | shipment = loads(shipments)[0] 137 | status_filter = shipment.get('status') 138 | retailer_id_filter = shipment.get('toId') 139 | dc_id_filter = shipment.get('fromId') 140 | shipments = shipment_service.get_shipments(self.loopback_token, status=status_filter, 141 | retailer_id=retailer_id_filter, dc_id=dc_id_filter) 142 | 143 | # Check that the shipments have correct values 144 | shipments_json = loads(shipments) 145 | for shipment_json in shipments_json: 146 | self.assertTrue(shipment_json.get('status') == status_filter) 147 | self.assertTrue(shipment_json.get('toId') == retailer_id_filter) 148 | self.assertTrue(shipment_json.get('fromId') == dc_id_filter) 149 | 150 | def test_get_shipments_invalid_token(self): 151 | """With an invalid token, are correct errors thrown?""" 152 | 153 | self.assertRaises(AuthenticationException, 154 | shipment_service.get_shipments, 155 | utils.get_bad_token()) 156 | 157 | def tearDown(self): 158 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 159 | 160 | 161 | class CreateShipmentTestCase(unittest.TestCase): 162 | """Tests for `services/shipments.py - create_shipment()`.""" 163 | 164 | def setUp(self): 165 | # Create demo 166 | self.demo = demo_service.create_demo() 167 | demo_json = loads(self.demo) 168 | demo_guid = demo_json.get('guid') 169 | demo_user_id = demo_json.get('users')[0].get('id') 170 | 171 | # Log in user 172 | auth_data = user_service.login(demo_guid, demo_user_id) 173 | self.loopback_token = auth_data.get('loopback_token') 174 | 175 | def test_create_shipment_success(self): 176 | """With correct values, is a valid shipment created?""" 177 | 178 | # Get retailers and distribution centers 179 | retailers = retailer_service.get_retailers(self.loopback_token) 180 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 181 | 182 | # Create shipment 183 | shipment = dict() 184 | shipment['fromId'] = loads(distribution_centers)[0].get('id') 185 | shipment['toId'] = loads(retailers)[0].get('id') 186 | shipment['estimatedTimeOfArrival'] = "2016-07-14" 187 | created_shipment = shipment_service.create_shipment(self.loopback_token, shipment) 188 | 189 | # TODO: Update to use assertIsInstance(a,b) 190 | # Check all expected object values are present 191 | shipment_json = loads(created_shipment) 192 | # Check that the shipment is valid 193 | self.assertTrue(shipment_json.get('id')) 194 | self.assertTrue(shipment_json.get('status')) 195 | self.assertTrue(shipment_json.get('createdAt')) 196 | self.assertTrue(shipment_json.get('fromId')) 197 | self.assertTrue(shipment_json.get('toId')) 198 | 199 | def test_create_shipment_invalid_ids(self): 200 | """With an invalid retailer/distribution center IDs, are correct errors thrown?""" 201 | 202 | # Get retailers and distribution centers 203 | retailers = retailer_service.get_retailers(self.loopback_token) 204 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 205 | 206 | # Create invalid shipments 207 | shipment_invalid_retailer = dict() 208 | shipment_invalid_retailer['fromId'] = loads(distribution_centers)[0].get('id') 209 | shipment_invalid_retailer['toId'] = "123321" 210 | shipment_invalid_dist = dict() 211 | shipment_invalid_dist['fromId'] = "123321" 212 | shipment_invalid_dist['toId'] = loads(retailers)[0].get('id') 213 | 214 | # Attempt to create a shipment with invalid IDs 215 | self.assertRaises(UnprocessableEntityException, 216 | shipment_service.create_shipment, 217 | self.loopback_token, shipment_invalid_retailer) 218 | self.assertRaises(UnprocessableEntityException, 219 | shipment_service.create_shipment, 220 | self.loopback_token, shipment_invalid_dist) 221 | 222 | def test_create_shipment_invalid_token(self): 223 | """With an invalid token, are correct errors thrown?""" 224 | 225 | # Get retailers and distribution centers 226 | retailers = retailer_service.get_retailers(self.loopback_token) 227 | distribution_centers = distribution_center_service.get_distribution_centers(self.loopback_token) 228 | 229 | # Create shipment 230 | shipment = dict() 231 | shipment['fromId'] = loads(distribution_centers)[0].get('id') 232 | shipment['toId'] = loads(retailers)[0].get('id') 233 | shipment['estimatedTimeOfArrival'] = "2016-07-14" 234 | created_shipment = shipment_service.create_shipment(self.loopback_token, shipment) 235 | 236 | # Attempt to create a shipment with invalid token 237 | self.assertRaises(AuthenticationException, 238 | shipment_service.create_shipment, 239 | utils.get_bad_token(), shipment) 240 | 241 | def tearDown(self): 242 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 243 | 244 | 245 | class GetShipmentTestCase(unittest.TestCase): 246 | """Tests for `services/shipments.py - get_shipment()`.""" 247 | 248 | def setUp(self): 249 | # Create demo 250 | self.demo = demo_service.create_demo() 251 | demo_json = loads(self.demo) 252 | demo_guid = demo_json.get('guid') 253 | demo_user_id = demo_json.get('users')[0].get('id') 254 | 255 | # Log in user 256 | auth_data = user_service.login(demo_guid, demo_user_id) 257 | self.loopback_token = auth_data.get('loopback_token') 258 | 259 | def test_get_shipment_success(self): 260 | """With correct values, is a valid shipment returned?""" 261 | 262 | # Get a shipment 263 | shipments = shipment_service.get_shipments(self.loopback_token) 264 | shipment_id = loads(shipments)[0].get('id') 265 | shipment = shipment_service.get_shipment(self.loopback_token, shipment_id) 266 | 267 | # TODO: Update to use assertIsInstance(a,b) 268 | # Check all expected object values are present 269 | shipment_json = loads(shipment) 270 | # Check that the shipment is valid 271 | self.assertTrue(shipment_json.get('id')) 272 | self.assertTrue(shipment_json.get('status')) 273 | self.assertTrue(shipment_json.get('createdAt')) 274 | self.assertTrue(shipment_json.get('estimatedTimeOfArrival')) 275 | self.assertTrue(shipment_json.get('fromId')) 276 | self.assertTrue(shipment_json.get('toId')) 277 | 278 | # Check that shipment address is valid, if present 279 | if shipment_json.get('currentLocation'): 280 | self.assertTrue(shipment_json.get('currentLocation').get('city')) 281 | self.assertTrue(shipment_json.get('currentLocation').get('state')) 282 | self.assertTrue(shipment_json.get('currentLocation').get('country')) 283 | self.assertTrue(shipment_json.get('currentLocation').get('latitude')) 284 | self.assertTrue(shipment_json.get('currentLocation').get('longitude')) 285 | 286 | # Check that the shipment's items are valid 287 | for item_json in shipment_json.get('items'): 288 | # Check that the item is valid 289 | self.assertTrue(item_json.get('id')) 290 | self.assertTrue(item_json.get('shipmentId')) 291 | self.assertTrue(item_json.get('productId')) 292 | self.assertTrue(item_json.get('quantity')) 293 | 294 | def test_get_shipment_no_items_filter_success(self): 295 | """With filter set to not include items, are they not returned?""" 296 | 297 | # Get a shipment 298 | shipments = shipment_service.get_shipments(self.loopback_token) 299 | shipment_id = loads(shipments)[0].get('id') 300 | shipment = shipment_service.get_shipment(self.loopback_token, shipment_id, include_items="0") 301 | 302 | # Make sure items are not returned 303 | self.assertFalse(loads(shipment).get('items')) 304 | 305 | def test_get_shipment_invalid_input(self): 306 | """With invalid inputs, are correct errors thrown?""" 307 | 308 | self.assertRaises(ResourceDoesNotExistException, 309 | shipment_service.get_shipment, 310 | self.loopback_token, '123321') 311 | 312 | def test_get_shipment_invalid_token(self): 313 | """With an invalid token, are correct errors thrown?""" 314 | 315 | # Get valid shipment ID 316 | shipments = shipment_service.get_shipments(self.loopback_token) 317 | shipment_id = loads(shipments)[0].get('id') 318 | 319 | # Attempt to get a shipment with invalid token 320 | self.assertRaises(AuthenticationException, 321 | shipment_service.get_shipment, 322 | utils.get_bad_token(), shipment_id) 323 | 324 | def tearDown(self): 325 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 326 | 327 | 328 | class DeleteShipmentTestCase(unittest.TestCase): 329 | """Tests for `services/shipments.py - delete_shipment()`.""" 330 | 331 | def setUp(self): 332 | # Create demo 333 | self.demo = demo_service.create_demo() 334 | demo_json = loads(self.demo) 335 | demo_guid = demo_json.get('guid') 336 | demo_user_id = demo_json.get('users')[0].get('id') 337 | 338 | # Log in user 339 | auth_data = user_service.login(demo_guid, demo_user_id) 340 | self.loopback_token = auth_data.get('loopback_token') 341 | 342 | def test_delete_shipment_success(self): 343 | """With correct values, is the shipment deleted?""" 344 | 345 | # Get a specific shipment 346 | shipments = shipment_service.get_shipments(self.loopback_token) 347 | shipment_id = loads(shipments)[0].get('id') 348 | 349 | # Delete shipment and check for successful return 350 | self.assertTrue(shipment_service.delete_shipment(self.loopback_token, shipment_id) is None) 351 | 352 | def test_delete_shipment_invalid_input(self): 353 | """With invalid inputs, are correct errors thrown?""" 354 | 355 | self.assertRaises(ResourceDoesNotExistException, 356 | shipment_service.delete_shipment, 357 | self.loopback_token, '123321') 358 | 359 | def test_delete_shipment_invalid_token(self): 360 | """With an invalid token, are correct errors thrown?""" 361 | 362 | # Get a specific shipment ID 363 | shipments = shipment_service.get_shipments(self.loopback_token) 364 | shipment_id = loads(shipments)[0].get('id') 365 | 366 | # Attempt to delete a shipment with invalid token 367 | self.assertRaises(AuthenticationException, 368 | shipment_service.delete_shipment, 369 | utils.get_bad_token(), shipment_id) 370 | 371 | def tearDown(self): 372 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 373 | 374 | 375 | class UpdateShipmentTestCase(unittest.TestCase): 376 | """Tests for `services/shipments.py - update_shipment()`.""" 377 | 378 | def setUp(self): 379 | # Create demo 380 | self.demo = demo_service.create_demo() 381 | demo_json = loads(self.demo) 382 | demo_guid = demo_json.get('guid') 383 | demo_user_id = demo_json.get('users')[0].get('id') 384 | 385 | # Log in user 386 | auth_data = user_service.login(demo_guid, demo_user_id) 387 | self.loopback_token = auth_data.get('loopback_token') 388 | 389 | def test_update_shipment_success(self): 390 | """With correct values, is the shipment updated?""" 391 | 392 | # Get a specific shipment 393 | shipments = shipment_service.get_shipments(self.loopback_token, status=statuses.pop(0)) 394 | shipment_id = loads(shipments)[0].get('id') 395 | 396 | # Iterate through shipment statuses and update shipment accordingly 397 | shipment = dict() 398 | for status in statuses: 399 | if isinstance(shipment, unicode): 400 | shipment = loads(shipment) 401 | shipment['status'] = status 402 | shipment = shipment_service.update_shipment(self.loopback_token, shipment_id, shipment) 403 | 404 | # TODO: Update to use assertIsInstance(a,b) 405 | # Check all expected object values are present 406 | shipment_json = loads(shipment) 407 | # Check that the shipments are valid 408 | self.assertTrue(shipment_json.get('id')) 409 | self.assertTrue(shipment_json.get('status') == status) 410 | self.assertTrue(shipment_json.get('createdAt')) 411 | self.assertTrue(shipment_json.get('estimatedTimeOfArrival')) 412 | self.assertTrue(shipment_json.get('fromId')) 413 | self.assertTrue(shipment_json.get('toId')) 414 | 415 | # Check that shipment address is valid, if present 416 | if shipment_json.get('currentLocation'): 417 | self.assertTrue(shipment_json.get('currentLocation').get('city')) 418 | self.assertTrue(shipment_json.get('currentLocation').get('state')) 419 | self.assertTrue(shipment_json.get('currentLocation').get('country')) 420 | self.assertTrue(shipment_json.get('currentLocation').get('latitude')) 421 | self.assertTrue(shipment_json.get('currentLocation').get('longitude')) 422 | 423 | def test_update_invalid_status(self): 424 | """With incorrect status updates, is the correct exception sent?""" 425 | 426 | # List of statuses progression 427 | prev_status = statuses[-1] 428 | for status in statuses: 429 | # Get an existing shipment with the current status 430 | shipments = shipment_service.get_shipments(self.loopback_token, status=status) 431 | shipment = loads(shipments)[0] 432 | shipment['status'] = prev_status 433 | 434 | # Attempt to update the status to an invalid value 435 | self.assertRaises(ValidationException, 436 | shipment_service.update_shipment, 437 | self.loopback_token, shipment.get('id'), shipment) 438 | prev_status = status 439 | 440 | def test_update_shipment_invalid_input(self): 441 | """With invalid inputs, are correct errors thrown?""" 442 | 443 | # Invalid shipment id 444 | shipment = dict() 445 | shipment['status'] = 'APPROVED' 446 | self.assertRaises(ResourceDoesNotExistException, 447 | shipment_service.update_shipment, 448 | self.loopback_token, '123321', shipment) 449 | 450 | def test_update_shipment_invalid_token(self): 451 | """With an invalid token, are correct errors thrown?""" 452 | 453 | # Get a specific shipment ID 454 | shipments = shipment_service.get_shipments(self.loopback_token) 455 | shipment_id = loads(shipments)[0].get('id') 456 | 457 | # Attempt to delete a shipment with invalid token 458 | shipment = dict() 459 | shipment['status'] = 'APPROVED' 460 | self.assertRaises(AuthenticationException, 461 | shipment_service.update_shipment, 462 | utils.get_bad_token(), shipment_id, shipment) 463 | 464 | def tearDown(self): 465 | demo_service.delete_demo_by_guid(loads(self.demo).get('guid')) 466 | 467 | if __name__ == '__main__': 468 | unittest.main() 469 | -------------------------------------------------------------------------------- /swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: "1.0.0" 4 | title: Acme Freight Controller 5 | description: The main controller app for the Acme Freight system, interfacing with the ERP, Recommendations, and UI services. 6 | license: 7 | name: Apache 2.0 8 | # the domain of the service 9 | host: acme-freight-controller.mybluemix.net 10 | # array of all schemes that your API supports 11 | schemes: 12 | - https 13 | # will be prefixed to all paths 14 | basePath: /api/v1 15 | produces: 16 | - application/json 17 | consumes: 18 | - application/json 19 | paths: 20 | '/demos': 21 | post: 22 | summary: Create Demo 23 | description: | 24 | Create new demo session 25 | tags: 26 | - Demo 27 | responses: 28 | 201: 29 | description: The new Demo object 30 | schema: 31 | $ref: '#/definitions/Demo' 32 | default: 33 | description: Unexpected error 34 | schema: 35 | $ref: '#/definitions/Error' 36 | '/demos/{guid}': 37 | get: 38 | summary: Get Demo 39 | description: | 40 | Retrieves the demo given the demo guid 41 | parameters: 42 | - name: guid 43 | in: path 44 | description: Demo guid 45 | required: true 46 | type: string 47 | tags: 48 | - User 49 | responses: 50 | 200: 51 | description: The retrieved Demo object 52 | schema: 53 | $ref: '#/definitions/Demo' 54 | 404: 55 | description: Demo does not exist 56 | schema: 57 | $ref: '#/definitions/Error' 58 | default: 59 | description: Unexpected error 60 | schema: 61 | $ref: '#/definitions/Error' 62 | delete: 63 | summary: Delete Demo 64 | description: | 65 | Deletes a Demo given the demo guid 66 | parameters: 67 | - name: guid 68 | in: path 69 | description: Demo guid 70 | required: true 71 | type: string 72 | tags: 73 | - Demo 74 | responses: 75 | 204: 76 | description: Demo deleted successfully 77 | 404: 78 | description: Demo does not exist 79 | schema: 80 | $ref: '#/definitions/Error' 81 | default: 82 | description: Unexpected error 83 | schema: 84 | $ref: '#/definitions/Error' 85 | '/demos/{guid}/retailers': 86 | get: 87 | summary: Get Demo retailers 88 | description: | 89 | Retrieves the retailers for a demo given the demo guid 90 | parameters: 91 | - name: guid 92 | in: path 93 | description: Demo guid 94 | required: true 95 | type: string 96 | tags: 97 | - Demo 98 | - Retailer 99 | responses: 100 | 200: 101 | description: The retrieved Retailers 102 | schema: 103 | type: array 104 | items: 105 | $ref: '#/definitions/Retailer' 106 | 404: 107 | description: Demo does not exist 108 | schema: 109 | $ref: '#/definitions/Error' 110 | default: 111 | description: Unexpected error 112 | schema: 113 | $ref: '#/definitions/Error' 114 | '/demos/{guid}/users': 115 | post: 116 | summary: Create Demo User 117 | description: | 118 | Create new user for the demo given demo guid 119 | parameters: 120 | - name: guid 121 | in: path 122 | description: Demo guid 123 | required: true 124 | type: string 125 | - name: retailerId 126 | in: body 127 | description: ID of assigned retailer 128 | required: true 129 | schema: 130 | type: string 131 | tags: 132 | - Demo 133 | - User 134 | responses: 135 | 201: 136 | description: The new User object 137 | schema: 138 | $ref: '#/definitions/User' 139 | 400: 140 | description: Invalid input 141 | schema: 142 | $ref: '#/definitions/Error' 143 | 404: 144 | description: Demo or Retailer does not exist 145 | schema: 146 | $ref: '#/definitions/Error' 147 | default: 148 | description: Unexpected error 149 | schema: 150 | $ref: '#/definitions/Error' 151 | '/demos/{guid}/login': 152 | post: 153 | summary: Sign In 154 | description: | 155 | Signs a specific user into a demo session 156 | parameters: 157 | - name: guid 158 | in: path 159 | description: Demo guid 160 | required: true 161 | type: string 162 | - name: userId 163 | in: body 164 | description: User ID 165 | required: true 166 | schema: 167 | type: string 168 | tags: 169 | - Demo 170 | - User 171 | responses: 172 | 200: 173 | description: The bearer token 174 | schema: 175 | $ref: '#/definitions/AccessToken' 176 | 404: 177 | description: Demo or User does not exist 178 | schema: 179 | $ref: '#/definitions/Error' 180 | default: 181 | description: Unexpected error 182 | schema: 183 | $ref: '#/definitions/Error' 184 | '/logout/{token}': 185 | delete: 186 | summary: User Logout 187 | description: | 188 | Logs out the user with the corresponding bearer token 189 | parameters: 190 | - name: Authorization 191 | in: header 192 | description: Bearer token 193 | required: true 194 | type: string 195 | - name: token 196 | in: path 197 | description: Token of the user to be logged out 198 | required: true 199 | type: string 200 | tags: 201 | - User 202 | responses: 203 | 204: 204 | description: Logout successful 205 | 401: 206 | description: Unauthorized 207 | schema: 208 | $ref: '#/definitions/Error' 209 | default: 210 | description: Unexpected error 211 | schema: 212 | $ref: '#/definitions/Error' 213 | '/admin': 214 | get: 215 | summary: Get initial demo data for logged in user 216 | description: | 217 | Retrieves data for the initial admin screen rendering 218 | parameters: 219 | - name: Authorization 220 | in: header 221 | description: Bearer token 222 | required: true 223 | type: string 224 | tags: 225 | - Shipment 226 | - Retailer 227 | - DistributionCenter 228 | responses: 229 | 200: 230 | description: The retrieved demo session objects 231 | schema: 232 | $ref: '#/definitions/Session' 233 | 401: 234 | description: Unauthorized 235 | schema: 236 | $ref: '#/definitions/Error' 237 | default: 238 | description: Unexpected error 239 | schema: 240 | $ref: '#/definitions/Error' 241 | '/shipments': 242 | get: 243 | summary: Retrieve shipments 244 | description: | 245 | Retrieves a list of all shipments for the supply chain managers 246 | parameters: 247 | - name: Authorization 248 | in: header 249 | description: Bearer token 250 | required: true 251 | type: string 252 | - name: status 253 | in: query 254 | description: Status of the shipments to be returned 255 | required: false 256 | type: string 257 | - name: did 258 | in: query 259 | description: Distribution center ID of the shipments to be returned 260 | required: false 261 | type: string 262 | - name: rid 263 | in: query 264 | description: Retailer ID of the shipments to be returned 265 | required: false 266 | type: string 267 | tags: 268 | - Shipment 269 | responses: 270 | 200: 271 | description: An array of shipments 272 | schema: 273 | type: array 274 | items: 275 | $ref: '#/definitions/Shipment' 276 | 401: 277 | description: Unauthorized 278 | schema: 279 | $ref: '#/definitions/Error' 280 | default: 281 | description: Unexpected error 282 | schema: 283 | $ref: '#/definitions/Error' 284 | post: 285 | summary: Create shipment 286 | description: | 287 | Create new shipment in the ERP system 288 | parameters: 289 | - name: shipment 290 | in: body 291 | description: ID of assigned retailer 292 | required: true 293 | schema: 294 | $ref: '#/definitions/Shipment' 295 | tags: 296 | - Shipment 297 | responses: 298 | 201: 299 | description: The new Shipment object 300 | schema: 301 | $ref: '#/definitions/Shipment' 302 | 400: 303 | description: Invalid input 304 | schema: 305 | $ref: '#/definitions/Error' 306 | 401: 307 | description: Unauthorized 308 | schema: 309 | $ref: '#/definitions/Error' 310 | 422: 311 | description: Required data is absent or invalid 312 | schema: 313 | $ref: '#/definitions/Error' 314 | default: 315 | description: Unexpected error 316 | schema: 317 | $ref: '#/definitions/Error' 318 | '/shipments/{id}': 319 | get: 320 | summary: Retrieve a single shipment from the ERP 321 | parameters: 322 | - name: Authorization 323 | in: header 324 | description: Bearer token 325 | required: true 326 | type: string 327 | - name: id 328 | in: path 329 | description: ID of the shipment to be retrieved 330 | required: true 331 | type: string 332 | - name: include_items 333 | in: query 334 | description: Boolean indicating whether to include LineItems in the returned shipment 335 | required: false 336 | type: string 337 | tags: 338 | - Shipment 339 | responses: 340 | 200: 341 | description: Shipment retrieved successfully 342 | schema: 343 | $ref: '#/definitions/Shipment' 344 | 401: 345 | description: Unauthorized 346 | schema: 347 | $ref: '#/definitions/Error' 348 | 404: 349 | description: Shipment does not exist 350 | schema: 351 | $ref: '#/definitions/Error' 352 | default: 353 | description: Unexpected error 354 | schema: 355 | $ref: '#/definitions/Error' 356 | delete: 357 | summary: Delete a shipment from the ERP 358 | parameters: 359 | - name: Authorization 360 | in: header 361 | description: Bearer token 362 | required: true 363 | type: string 364 | - name: id 365 | in: path 366 | description: ID of the shipment to be deleted 367 | required: true 368 | type: string 369 | tags: 370 | - Shipment 371 | responses: 372 | 204: 373 | description: Shipment deleted successfully 374 | 401: 375 | description: Unauthorized 376 | schema: 377 | $ref: '#/definitions/Error' 378 | 404: 379 | description: Shipment does not exist 380 | schema: 381 | $ref: '#/definitions/Error' 382 | default: 383 | description: Unexpected error 384 | schema: 385 | $ref: '#/definitions/Error' 386 | put: 387 | summary: Update a shipment in the ERP 388 | parameters: 389 | - name: Authorization 390 | in: header 391 | description: Bearer token 392 | required: true 393 | type: string 394 | - name: id 395 | in: path 396 | description: ID of the shipment to be updated 397 | required: true 398 | type: string 399 | - name: data 400 | in: body 401 | description: The shipment object to replace the object currently associated with input ID 402 | required: false 403 | schema: 404 | $ref: '#/definitions/Shipment' 405 | tags: 406 | - Shipment 407 | responses: 408 | 200: 409 | description: Shipment updated successfully 410 | schema: 411 | $ref: '#/definitions/Shipment' 412 | 400: 413 | description: Invalid update to shipment 414 | schema: 415 | $ref: '#/definitions/Error' 416 | 401: 417 | description: Unauthorized 418 | schema: 419 | $ref: '#/definitions/Error' 420 | 404: 421 | description: Shipment does not exist 422 | schema: 423 | $ref: '#/definitions/Error' 424 | default: 425 | description: Unexpected error 426 | schema: 427 | $ref: '#/definitions/Error' 428 | '/distribution-centers': 429 | get: 430 | summary: Retrieve distribution centers 431 | description: | 432 | Retrieves a list of distribution centers, only for the supply chain managers 433 | parameters: 434 | - name: Authorization 435 | in: header 436 | description: Bearer token 437 | required: true 438 | type: string 439 | tags: 440 | - DistributionCenter 441 | responses: 442 | 200: 443 | description: Retrieved distribution centers successfully 444 | schema: 445 | type: array 446 | items: 447 | $ref: '#/definitions/DistributionCenter' 448 | 401: 449 | description: Unauthorized 450 | schema: 451 | $ref: '#/definitions/Error' 452 | default: 453 | description: Unexpected error 454 | schema: 455 | $ref: '#/definitions/Error' 456 | '/distribution-centers/{id}': 457 | get: 458 | summary: Retrieve distribution center from the ERP 459 | parameters: 460 | - name: Authorization 461 | in: header 462 | description: Bearer token 463 | required: true 464 | type: string 465 | - name: id 466 | in: path 467 | description: ID of the distribution center to be retrieved 468 | required: true 469 | type: string 470 | tags: 471 | - DistributionCenter 472 | responses: 473 | 200: 474 | description: Distribution center retrieved successfully 475 | schema: 476 | $ref: '#/definitions/DistributionCenter' 477 | 401: 478 | description: Unauthorized 479 | schema: 480 | $ref: '#/definitions/Error' 481 | 404: 482 | description: Distribution center does not exist 483 | schema: 484 | $ref: '#/definitions/Error' 485 | default: 486 | description: Unexpected error 487 | schema: 488 | $ref: '#/definitions/Error' 489 | '/distribution-centers/{id}/shipments': 490 | get: 491 | summary: Retrieve shipments for a specific distribution center 492 | parameters: 493 | - name: Authorization 494 | in: header 495 | description: Bearer token 496 | required: true 497 | type: string 498 | - name: id 499 | in: path 500 | description: ID of the distribution center for which shipments are to be retrieved 501 | required: true 502 | type: string 503 | - name: status 504 | in: query 505 | description: Status of the shipments to be returned 506 | required: false 507 | type: string 508 | tags: 509 | - DistributionCenter 510 | - Shipment 511 | responses: 512 | 200: 513 | description: Shipments retrieved successfully 514 | schema: 515 | $ref: '#/definitions/Shipment' 516 | 401: 517 | description: Unauthorized 518 | schema: 519 | $ref: '#/definitions/Error' 520 | 404: 521 | description: Distribution center does not exist 522 | schema: 523 | $ref: '#/definitions/Error' 524 | default: 525 | description: Unexpected error 526 | schema: 527 | $ref: '#/definitions/Error' 528 | '/distribution-centers/{id}/inventory': 529 | get: 530 | summary: Retrieve inventory for a specific distribution center 531 | parameters: 532 | - name: Authorization 533 | in: header 534 | description: Bearer token 535 | required: true 536 | type: string 537 | - name: id 538 | in: path 539 | description: ID of the distribution center for which inventory is to be retrieved 540 | required: true 541 | type: string 542 | tags: 543 | - DistributionCenter 544 | - Inventory 545 | responses: 546 | 200: 547 | description: Inventory retrieved successfully 548 | schema: 549 | type: array 550 | items: 551 | $ref: '#/definitions/Inventory' 552 | 401: 553 | description: Unauthorized 554 | schema: 555 | $ref: '#/definitions/Error' 556 | 404: 557 | description: Distribution center does not exist 558 | schema: 559 | $ref: '#/definitions/Error' 560 | default: 561 | description: Unexpected error 562 | schema: 563 | $ref: '#/definitions/Error' 564 | '/retailers': 565 | get: 566 | summary: Retrieves retailers 567 | description: | 568 | Retrieves a list of all retailers, only for supply chain managers 569 | parameters: 570 | - name: Authorization 571 | in: header 572 | description: Bearer token 573 | required: true 574 | type: string 575 | tags: 576 | - Retailer 577 | responses: 578 | 200: 579 | description: Retrieved retailers successfully 580 | schema: 581 | type: array 582 | items: 583 | $ref: '#/definitions/Retailer' 584 | 401: 585 | description: Unauthorized 586 | schema: 587 | $ref: '#/definitions/Error' 588 | default: 589 | description: Unexpected error 590 | schema: 591 | $ref: '#/definitions/Error' 592 | '/retailers/{id}': 593 | get: 594 | summary: Retrieve retailer from the ERP 595 | parameters: 596 | - name: Authorization 597 | in: header 598 | description: Bearer token 599 | required: true 600 | type: string 601 | - name: id 602 | in: path 603 | description: ID of the retailer to be retrieved 604 | required: true 605 | type: string 606 | tags: 607 | - Retailer 608 | responses: 609 | 200: 610 | description: Retailer retrieved successfully 611 | schema: 612 | $ref: '#/definitions/Retailer' 613 | 401: 614 | description: Unauthorized 615 | schema: 616 | $ref: '#/definitions/Error' 617 | 404: 618 | description: Retailer does not exist 619 | schema: 620 | $ref: '#/definitions/Error' 621 | default: 622 | description: Unexpected error 623 | schema: 624 | $ref: '#/definitions/Error' 625 | '/retailers/{id}/shipments': 626 | get: 627 | summary: Retrieve shipments for a specific retailer 628 | parameters: 629 | - name: Authorization 630 | in: header 631 | description: Bearer token 632 | required: true 633 | type: string 634 | - name: id 635 | in: path 636 | description: ID of the retailer for which shipments are to be retrieved 637 | required: true 638 | type: string 639 | - name: status 640 | in: query 641 | description: Status of the shipments to be returned 642 | required: false 643 | type: string 644 | tags: 645 | - Retailer 646 | - Shipment 647 | responses: 648 | 200: 649 | description: Shipments retrieved successfully 650 | schema: 651 | $ref: '#/definitions/Shipment' 652 | 401: 653 | description: Unauthorized 654 | schema: 655 | $ref: '#/definitions/Error' 656 | 404: 657 | description: Retailer does not exist 658 | schema: 659 | $ref: '#/definitions/Error' 660 | default: 661 | description: Unexpected error 662 | schema: 663 | $ref: '#/definitions/Error' 664 | '/retailers/{id}/sales': 665 | get: 666 | summary: Retrieve sales data for a specific retailer 667 | parameters: 668 | - name: Authorization 669 | in: header 670 | description: Bearer token 671 | required: true 672 | type: string 673 | - name: id 674 | in: path 675 | description: ID of the retailer for which sales data is to be retrieved 676 | required: true 677 | type: string 678 | tags: 679 | - Retailer 680 | responses: 681 | 200: 682 | description: Sales data retrieved successfully 683 | schema: 684 | type: array 685 | items: 686 | $ref: '#/definitions/SalesData' 687 | 401: 688 | description: Unauthorized 689 | schema: 690 | $ref: '#/definitions/Error' 691 | 404: 692 | description: Retailer does not exist 693 | schema: 694 | $ref: '#/definitions/Error' 695 | default: 696 | description: Unexpected error 697 | schema: 698 | $ref: '#/definitions/Error' 699 | '/retailers/{id}/inventory': 700 | get: 701 | summary: Retrieve inventory for a specific retailer 702 | parameters: 703 | - name: Authorization 704 | in: header 705 | description: Bearer token 706 | required: true 707 | type: string 708 | - name: id 709 | in: path 710 | description: ID of the retailer for which inventory is to be retrieved 711 | required: true 712 | type: string 713 | tags: 714 | - Retailer 715 | - Inventory 716 | responses: 717 | 200: 718 | description: Inventory retrieved successfully 719 | schema: 720 | type: array 721 | items: 722 | $ref: '#/definitions/Inventory' 723 | 401: 724 | description: Unauthorized 725 | schema: 726 | $ref: '#/definitions/Error' 727 | 404: 728 | description: Retailer does not exist 729 | schema: 730 | $ref: '#/definitions/Error' 731 | default: 732 | description: Unexpected error 733 | schema: 734 | $ref: '#/definitions/Error' 735 | '/products': 736 | get: 737 | summary: Retrieves products 738 | description: | 739 | Retrieves all products distributed by the organization 740 | parameters: 741 | - name: Authorization 742 | in: header 743 | description: Bearer token 744 | required: true 745 | type: string 746 | tags: 747 | - Product 748 | responses: 749 | 200: 750 | description: Retrieved products successfully 751 | schema: 752 | type: array 753 | items: 754 | $ref: '#/definitions/Product' 755 | 401: 756 | description: Unauthorized 757 | schema: 758 | $ref: '#/definitions/Error' 759 | default: 760 | description: Unexpected error 761 | schema: 762 | $ref: '#/definitions/Error' 763 | '/weather/recommendations': 764 | get: 765 | summary: Get Recommendations 766 | description: | 767 | Get recommendations for a demo session 768 | parameters: 769 | - name: Authorization 770 | in: header 771 | description: Bearer token 772 | required: true 773 | type: string 774 | tags: 775 | - Weather 776 | responses: 777 | 200: 778 | description: The recommendations 779 | '/weather/acknowledge': 780 | post: 781 | summary: Acknowledge a recommendation 782 | description: | 783 | Acknowledge a recommendation 784 | parameters: 785 | - name: Authorization 786 | in: header 787 | description: Bearer token 788 | required: true 789 | type: string 790 | - name: id 791 | in: body 792 | description: Recommendation Id 793 | required: true 794 | type: string 795 | tags: 796 | - Weather 797 | responses: 798 | 200: 799 | description: The recommendations 800 | '/weather/simulate': 801 | post: 802 | summary: Trigger a simulation 803 | description: | 804 | Trigger a simulation 805 | parameters: 806 | - name: Authorization 807 | in: header 808 | description: Bearer token 809 | required: true 810 | type: string 811 | tags: 812 | - Weather 813 | responses: 814 | 200: 815 | description: Upon successful triggering 816 | '/weather/observations': 817 | post: 818 | summary: Return weather observations for the given location 819 | description: | 820 | Return weather observations for the given location 821 | parameters: 822 | - name: Authorization 823 | in: header 824 | description: Bearer token 825 | required: true 826 | type: string 827 | - name: latitude 828 | in: body 829 | description: Latitude of the location to lookup 830 | required: true 831 | type: number 832 | - name: longitude 833 | in: body 834 | description: Longitude of the location to lookup 835 | required: true 836 | type: number 837 | tags: 838 | - Weather 839 | responses: 840 | 200: 841 | description: | 842 | An aggregation of th current weather observation, 10-day forecasts and 843 | alerts results from the Weather Company Data service 844 | definitions: 845 | Demo: 846 | properties: 847 | id: 848 | type: integer 849 | format: int32 850 | guid: 851 | type: string 852 | createdAt: 853 | type: string 854 | format: date 855 | users: 856 | type: array 857 | items: 858 | $ref: '#/definitions/User' 859 | Session: 860 | properties: 861 | shipments: 862 | type: array 863 | items: 864 | $ref: '#/definitions/Shipment' 865 | distributionCenters: 866 | type: array 867 | items: 868 | $ref: '#/definitions/DistributionCenter' 869 | retailers: 870 | type: array 871 | items: 872 | $ref: '#/definitions/Retailer' 873 | required: 874 | - retailers 875 | User: 876 | properties: 877 | id: 878 | type: integer 879 | format: int32 880 | demoId: 881 | type: integer 882 | format: int32 883 | username: 884 | type: string 885 | email: 886 | type: string 887 | roles: 888 | type: array 889 | items: 890 | $ref: '#/definitions/Role' 891 | created: 892 | type: string 893 | format: date 894 | lastUpdated: 895 | type: string 896 | format: date 897 | required: 898 | - email 899 | Role: 900 | properties: 901 | id: 902 | type: string 903 | name: 904 | type: string 905 | created: 906 | type: string 907 | format: date 908 | modified: 909 | type: string 910 | format: date 911 | required: 912 | - name 913 | AccessToken: 914 | properties: 915 | token: 916 | type: string 917 | required: 918 | - token 919 | additionalProperties: false 920 | Product: 921 | properties: 922 | name: 923 | type: string 924 | id: 925 | type: integer 926 | format: int32 927 | supplierId: 928 | type: string 929 | required: 930 | - name 931 | Shipment: 932 | properties: 933 | status: 934 | default: NEW 935 | type: string 936 | createdAt: 937 | type: string 938 | format: date 939 | updatedAt: 940 | type: string 941 | format: date 942 | deliveredAt: 943 | type: string 944 | format: date 945 | estimatedTimeOfArrival: 946 | type: string 947 | format: date 948 | id: 949 | type: integer 950 | format: int32 951 | currentLocation: 952 | $ref: '#/definitions/Address' 953 | fromId: 954 | type: integer 955 | format: int32 956 | toId: 957 | type: integer 958 | format: int32 959 | items: 960 | type: array 961 | items: 962 | $ref: '#/definitions/LineItem' 963 | required: 964 | - status 965 | - createdAt 966 | additionalProperties: false 967 | Inventory: 968 | properties: 969 | quantity: 970 | default: 0 971 | type: number 972 | format: double 973 | id: 974 | type: integer 975 | format: int32 976 | productId: 977 | type: integer 978 | format: int32 979 | locationId: 980 | type: integer 981 | format: int32 982 | locationType: 983 | type: string 984 | additionalProperties: false 985 | LineItem: 986 | properties: 987 | quantity: 988 | default: 0 989 | type: number 990 | format: double 991 | id: 992 | type: integer 993 | format: int32 994 | shipmentId: 995 | type: integer 996 | format: int32 997 | required: 998 | - quantity 999 | additionalProperties: false 1000 | DistributionCenter: 1001 | properties: 1002 | id: 1003 | type: integer 1004 | format: int32 1005 | address: 1006 | $ref: '#/definitions/Address' 1007 | contact: 1008 | $ref: '#/definitions/Contact' 1009 | inventory: 1010 | $ref: '#/definitions/Inventory' 1011 | additionalProperties: false 1012 | Retailer: 1013 | properties: 1014 | id: 1015 | type: integer 1016 | format: int32 1017 | address: 1018 | $ref: '#/definitions/Address' 1019 | contact: 1020 | $ref: '#/definitions/Contact' 1021 | additionalProperties: false 1022 | Address: 1023 | properties: 1024 | city: 1025 | type: string 1026 | state: 1027 | type: string 1028 | country: 1029 | type: string 1030 | latitude: 1031 | type: number 1032 | format: double 1033 | longitude: 1034 | type: number 1035 | format: double 1036 | id: 1037 | type: integer 1038 | format: int32 1039 | additionalProperties: false 1040 | Contact: 1041 | properties: 1042 | name: 1043 | type: string 1044 | id: 1045 | type: integer 1046 | format: int32 1047 | required: 1048 | - name 1049 | additionalProperties: false 1050 | SalesData: 1051 | properties: 1052 | productId: 1053 | type: string 1054 | sales: 1055 | type: array 1056 | items: 1057 | $ref: '#/definitions/MonthlySales' 1058 | MonthlySales: 1059 | properties: 1060 | month: 1061 | type: string 1062 | amount: 1063 | type: integer 1064 | Error: 1065 | type: object 1066 | properties: 1067 | code: 1068 | type: integer 1069 | format: int32 1070 | message: 1071 | type: string 1072 | user_details: 1073 | type: string 1074 | tags: 1075 | - name: Supplier 1076 | - name: Product 1077 | - name: DistributionCenter 1078 | - name: Shipment 1079 | - name: Retailer 1080 | - name: Inventory 1081 | - name: User 1082 | - name: Demo 1083 | - name: Role 1084 | --------------------------------------------------------------------------------