[
  {
    "path": ".dockerignore",
    "content": ".dockerignore\n.git\n.gitignore\n.idea\nvenv\n__pycache__\n\nlearning-data\n\nconfig-*.env\ndocker-compose.yml\nDockerfile\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"pip\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n    assignees:\n      - \"heldplayer\"\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ master ]\n#   schedule:\n#     - cron: '0 17 * * 3'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file. \n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v1\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Prepare\n      run: touch config-prod.env config-dev.env\n\n    - name: Build the Docker image\n      run: |\n        docker network prune -f\n        docker-compose build komidabot-dev komidabot-db\n\n    - name: Start supporting services\n      run: docker-compose up -d komidabot-db\n\n    - name: Run the tests\n      run: docker-compose run --rm komidabot-dev python -W default manage.py test\n\n    - name: Cleanup\n      run: docker-compose stop\n"
  },
  {
    "path": ".gitignore",
    "content": "*.env\ndump*.txt\n*.pem\n\n.idea/\n__pycache__/\nvenv/\n\nout.png\npage.xml\n\nscratches/\n"
  },
  {
    "path": "Dockerfile",
    "content": "# base image\nFROM python:3.10-slim\n\nENV TZ=Europe/Brussels\n\n# install dependencies\nRUN set -eu ; \\\n    apt-get -qq update ; \\\n    apt-get -y -qq upgrade ; \\\n    apt-get -y -qq install netcat-openbsd bash ; \\\n    apt-get -y -qq install gcc build-essential ; \\\n#    apt-get -y -qq install postgresql-dev ; \\\n    apt-get -y -qq install libxml2 libxml2-dev libxslt1.1 libxslt1-dev libjpeg-dev poppler-utils ; \\\n    apt-get -y -qq install locales-all\n\n# set working directory\nWORKDIR /usr/src/app\n\n# add and install requirements\nCOPY ./requirements.txt /usr/src/app/requirements.txt\nRUN pip install --upgrade pip\nRUN pip install --no-cache-dir -r requirements.txt\n\n# get some space back\n#RUN apk del build-deps\nRUN set -eu ; \\\n    apt-get -y -qq autoremove gcc build-essential ; \\\n    apt-get clean\n\n# add entrypoint.sh\nCOPY ./entrypoint.sh /usr/src/app/entrypoint.sh\nRUN chmod +x /usr/src/app/entrypoint.sh\n\n# add app\nCOPY . /usr/src/app\n\n# run server\nENTRYPOINT [\"/bin/bash\", \"/usr/src/app/entrypoint.sh\"]\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: test run-prod run-dev stop\n\ntest:\n\tdocker-compose build komidabot-dev && \\\n\tdocker-compose run --rm komidabot-dev python -W default manage.py test\n\nrun-prod:\n\tdocker-compose up --build komidabot-prod\n\nrun-dev:\n\tdocker-compose up --build komidabot-dev\n\nstop:\n\tdocker-compose stop\n"
  },
  {
    "path": "README.md",
    "content": "# komidabot-docker\n\n![CodeQL](https://github.com/heldplayer/komidabot-docker/workflows/CodeQL/badge.svg)\n![Tests](https://github.com/heldplayer/komidabot-docker/workflows/Tests/badge.svg)\n"
  },
  {
    "path": "app.py",
    "content": "import locale\nimport logging\nimport os\n\nfrom flask import Flask\nfrom flask.cli import ScriptInfo\nfrom werkzeug.middleware.proxy_fix import ProxyFix\n\nfrom extensions import db, login, migrate, session\nfrom komidabot.app import App as KomidabotApp\nfrom komidabot.features import update_active_features\n\n\ndef create_app(*, app_settings: str = None):\n    locale.setlocale(locale.LC_MONETARY, 'nl_BE.utf8')\n\n    # instantiate the app\n    app = Flask(__name__)\n    app.wsgi_app = ProxyFix(app.wsgi_app)\n\n    # set config\n    if app_settings is None:\n        app_settings = os.getenv('APP_SETTINGS')\n    app.config.from_object(app_settings)\n\n    # print(\"The script config is\", script_info, flush=True)\n    # print(\" - Data: \", script_info.data, flush=True)\n    # print(\"The database URI is\", app.config.get('SQLALCHEMY_DATABASE_URI'), flush=True)\n\n    # set up extensions\n    session.init_app(app)\n    db.init_app(app)\n    migrate.init_app(app)\n    login.init_app(app)\n\n    # Make sure database models are registered\n    # noinspection PyUnresolvedReferences\n    import komidabot.models\n    # noinspection PyUnresolvedReferences\n    import komidabot.models_training\n    # noinspection PyUnresolvedReferences\n    import komidabot.models_users\n\n    # register blueprints\n    from komidabot.blueprint import blueprint as webhook_blueprint\n    from komidabot.blueprint_api import blueprint as api_blueprint\n    from komidabot.blueprint_authentication import blueprint as authentication_blueprint\n\n    app.register_blueprint(webhook_blueprint, url_prefix='/webhook')\n    app.register_blueprint(api_blueprint, url_prefix='/api')\n    app.register_blueprint(authentication_blueprint, url_prefix='/api')  # Shares the api prefix\n\n    # shell context for flask cli\n    @app.shell_context_processor\n    def ctx():\n        return {'app': app, 'db': db}\n\n    app.logger.setLevel(logging.DEBUG)\n\n    if os.environ.get(\"KOMIDABOT_SKIP_INITIALISATION\") == \"true\":\n        # Don't initialise anything if run from the CLI\n        return app\n\n    if app.config['TESTING']:\n        # noinspection PyCallByClass,PyTypeChecker\n        KomidabotApp.__init__(app, app.config)\n\n        return app\n\n    if not app.debug or os.environ.get(\"WERKZEUG_RUN_MAIN\") == \"true\":\n        if os.environ.get(\"WERKZEUG_RUN_MAIN\") == \"true\":\n            print(\" * Worker processes PID: {}\".format(os.getpid()), flush=True)\n\n        with app.app_context():\n            update_active_features()\n\n        # TODO: Check if we need to initialise the database and blueprints only once as well\n\n        # The app is not in debug mode or we are in the reloaded process\n        # noinspection PyCallByClass,PyTypeChecker\n        KomidabotApp.__init__(app, app.config)\n\n    return app\n"
  },
  {
    "path": "breaking-responses/cde-2020-10-26.json",
    "content": "{\n  \"COMMENT\": \"Tomato soup appears twice in this response, which causes issues when the bot tries to update the menu\",\n  \"id\": 1072,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8760,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 11148,\n          \"menuItemId\": 8760,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8410,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 10718,\n          \"menuItemId\": 8410,\n          \"courseId\": 2323,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2323,\n            \"dispNameNl\": \"Bladerdeeg met geitenkaas, rauwkostsalade en frietjes\",\n            \"dispNameEn\": \"xx Goat cheese puff pastry with crucités and French fries\",\n            \"nameNl\": \"bladerdeeg geitenkaas (rauwkostsalade + frietjes), zvv, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"bladerdeegjes openleggen op platte plaatjes met boterpapier. - afbakken op 180 °c. + zomerslaatje erbij serveren (zie lijst zomer rauwkostsalades)\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2323,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 2323,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9108,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 11504,\n          \"menuItemId\": 9108,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8679,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11056,\n          \"menuItemId\": 8679,\n          \"courseId\": 5226,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5226,\n            \"dispNameNl\": \"Regenboog quinoa bowl\",\n            \"dispNameEn\": \"Rainbow quinoa bowl\",\n            \"nameNl\": \"00 regenboog quinoa bowl,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rode quinoa klaar volgens de bereidingswijze op de verpakking en laat afkoelen.- maak de humusdressing met sojayoghurt en humus, breng op smaak met peper en zout. - maak de bowl: doe de quinoa in een kom, vervolgens baby leaf, rode en groene paprikareepjes, reepjes komkommer, kerstomaten en kikkererwten.- werk af met de prei scheuten, granaatappelpitjes, sesamzaad en de tahini dressing\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8689,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11066,\n          \"menuItemId\": 8689,\n          \"courseId\": 3488,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3488,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - lenteui schuin versnijden, mengen met sojascheuten en paprikablokjes en kokoschilfers- opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8709,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11086,\n          \"menuItemId\": 8709,\n          \"courseId\": 1865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1865,\n            \"dispNameNl\": \"Salade Dolce Vita\",\n            \"dispNameEn\": \"Dolce Vita salad\",\n            \"nameNl\": \"salade dolce vita (penne, mozzarella, courgette), dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"meer dan de helft van de salade moet bestaan uit groenten. courgette en aubergine snijden en kort roosteren.pasta koken. - kerstomaat en notensla wassen - olijven laten uitlekken. groenten mengen en afsmaken.- pasta groenten en mozzarella assembleren afwerken met notensla en hennepzaad\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9128,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11538,\n          \"menuItemId\": 9128,\n          \"courseId\": 525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 525,\n            \"dispNameNl\": \"Caesar salad on a bun\",\n            \"dispNameEn\": \"Caesar salad on a bun\",\n            \"nameNl\": \"caesar salad on a bun, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 525,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 525,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9135,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11545,\n          \"menuItemId\": 9135,\n          \"courseId\": 4169,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4169,\n            \"dispNameNl\": \"Nordic cottage cheese\",\n            \"dispNameEn\": \"Nordic cottage cheese\",\n            \"nameNl\": \"cottage cheese nordic (ger. zalm)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9138,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11548,\n          \"menuItemId\": 9138,\n          \"courseId\": 1552,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1552,\n            \"dispNameNl\": \"Crunchy yoghurt\",\n            \"dispNameEn\": \"Crunchy yoghurt\",\n            \"nameNl\": \"crunchy yoghurt\",\n            \"nameEn\": \"\",\n            \"weight\": \"300ml\",\n            \"extra\": null,\n            \"preparation\": \"potjes vullen met krieken en bijvullen met yoghurt - dekseltje vullen met crunchy en op potje zetten keuze om volle of magere yoghurt te gebruiken keuze uit krieken bereid uit blik of krieken uit bokaal (wat je in stock heb)\",\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1552,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9141,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11551,\n          \"menuItemId\": 9141,\n          \"courseId\": 1532,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1532,\n            \"dispNameNl\": \"Panna cotta\",\n            \"dispNameEn\": \"Panna cotta\",\n            \"nameNl\": \"panna cotta\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"garnituur: rote grutze/speculoos\",\n            \"preparation\": \"zie verpakking - speculoos of rote grutze er bovenop serveren\",\n            \"price\": 1.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1532,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9200,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11617,\n          \"menuItemId\": 9200,\n          \"courseId\": 3283,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3283,\n            \"dispNameNl\": \"New York cheesecake\",\n            \"dispNameEn\": \"New York cheese cake\",\n            \"nameNl\": \"new york cheesecake\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3283,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9206,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11623,\n          \"menuItemId\": 9206,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "breaking-responses/cmu-2020-09-25.json",
    "content": "{\n  \"COMMENT\": \"Komidabot interpreted the 9th item (Rijstpap) as a main course because there is no snack or salad specifier.\",\n  \"id\": 983,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7575,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9808,\n          \"menuItemId\": 7575,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"‘Classic’ tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan dressing erover - dan basilicum - zonnebloempitten en romeinse sla bovenaan - geen dressingpotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7592,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9823,\n          \"menuItemId\": 7592,\n          \"courseId\": 3866,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3866,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter 6 falafels per persoon- pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox gele wortel in fijne reepjes snijden (optioneel toevoegen) hzs: falafel in oven bakken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7581,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9813,\n          \"menuItemId\": 7581,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7598,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9829,\n          \"menuItemId\": 7598,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7605,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9835,\n          \"menuItemId\": 7605,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7611,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9967,\n          \"menuItemId\": 7611,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7617,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 9845,\n          \"menuItemId\": 7617,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7740,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9980,\n          \"menuItemId\": 7740,\n          \"courseId\": 1109,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1109,\n            \"dispNameNl\": \"Hamburger\",\n            \"dispNameEn\": \"Hamburger\",\n            \"nameNl\": \"hamburger standaard, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": \"keuze om ketchup 3 liter of 1 liter te gebruiken\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7744,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9984,\n          \"menuItemId\": 7744,\n          \"courseId\": 1537,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1537,\n            \"dispNameNl\": \"Rijstpap\",\n            \"dispNameEn\": \"Rice pudding\",\n            \"nameNl\": \"rijstpap kant-en klaar\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"garnituur: bruine suiker\",\n            \"preparation\": \"potjes vullen met de kant- en klare rijstpap en bruine suiker erbij serveren\",\n            \"price\": 1.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1537,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1537,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "commands.txt",
    "content": "\nCreate a new migration script after a schema change:\n\ndocker-compose exec komidabot-dev flask db migrate\n\n\nRun tests:\n\ndocker-compose exec komidabot-dev python manage.py test\n"
  },
  {
    "path": "config.py",
    "content": "import os\nfrom collections import namedtuple\nfrom typing import List, Optional, TypedDict\n\nPOSTGRES_HOST = os.getenv('POSTGRES_HOST', 'localhost')\nPOSTGRES_USER = os.getenv('POSTGRES_USER', 'postgres')\nPOSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD', '')\n\n# NOTE: While this is a different namedtuple from UserId, this will still properly handle equality checks between other\n#       named tuples (including typing.NamedTuple)\n_UserId = namedtuple('_UserId', ['id', 'provider'])\n\n\ndef _get_user(string: str) -> _UserId:\n    split = string.split('/', 2)\n    if len(split) == 1:\n        return _UserId(split[0], 'facebook')\n    else:\n        return _UserId(split[1], split[0])\n\n\ndef _get_postgres_uri(host, user, password, db):\n    if not db:\n        raise ValueError('Invalid database')\n    if password:\n        return f'postgresql://{user}:{password}@{host}:5432/{db}'\n    else:\n        return f'postgresql://{user}@{host}:5432/{db}'\n\n\nclass ConfigType(TypedDict):\n    TESTING: bool\n    TESTING: bool\n    PRODUCTION: bool\n    DISABLED: bool\n    VERBOSE: bool\n    DUMP_FILE: Optional[str]\n\n    PAGE_ACCESS_TOKEN: Optional[str]\n    VERIFY_TOKEN: Optional[str]\n    APP_SECRET: Optional[str]\n\n    ADMIN_IDS: List[_UserId]\n\n    VAPID_PRIVATE_KEY: Optional[str]\n    VAPID_PUBLIC_KEY: Optional[str]\n\n    AUTH_GOOGLE_CLIENT_ID: Optional[str]\n    AUTH_GOOGLE_CLIENT_SECRET: Optional[str]\n    AUTH_GOOGLE_DISCOVERY_URL: str\n\n    COVID19_DISABLED: int\n\n\nclass BaseConfig:\n    \"\"\"Base configuration\"\"\"\n    PRODUCTION = False\n    DISABLED = int(os.getenv('DISABLED', '0')) != 0\n    VERBOSE = int(os.getenv('VERBOSE', '0')) != 0\n    DUMP_FILE = os.getenv('DUMP_FILE')\n\n    PAGE_ACCESS_TOKEN = os.getenv('PAGE_ACCESS_TOKEN')\n    VERIFY_TOKEN = os.getenv('VERIFY_TOKEN')\n    APP_SECRET = os.getenv('APP_SECRET')\n\n    ADMIN_IDS = [_get_user(split) for split in os.getenv('ADMIN_IDS', '').split(':')]\n\n    VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')\n    VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')\n\n    AUTH_GOOGLE_CLIENT_ID = os.environ.get(\"GOOGLE_CLIENT_ID\", None)\n    AUTH_GOOGLE_CLIENT_SECRET = os.environ.get(\"GOOGLE_CLIENT_SECRET\", None)\n    AUTH_GOOGLE_DISCOVERY_URL = 'https://accounts.google.com/.well-known/openid-configuration'\n\n    COVID19_DISABLED = int(os.getenv('COVID19_DISABLED', '0')) != 0\n\n    # Flask options\n    SESSION_REFRESH_EACH_REQUEST = False\n\n    # Flask-SQLAlchemy options\n    SQLALCHEMY_TRACK_MODIFICATIONS = False\n\n    # Flask-Session options\n    SESSION_COOKIE_PATH = '/api/'\n    SESSION_COOKIE_HTTPONLY = True\n    SESSION_COOKIE_SECURE = int(os.getenv('LIVE_VERSION', '0')) != 0\n    SESSION_COOKIE_SAMESITE = 'Lax'\n    SESSION_TYPE = 'filesystem'\n    SESSION_PERMANENT = False\n    SESSION_FILE_DIR = '/var/flask_session'\n\n\nclass ProductionConfig(BaseConfig):\n    \"\"\"Production configuration\"\"\"\n    PRODUCTION = True\n\n    # Flask-SQLAlchemy options\n    SQLALCHEMY_DATABASE_URI = _get_postgres_uri(POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, 'komidabot_prod')\n\n\nclass DevelopmentConfig(BaseConfig):\n    \"\"\"Development configuration\"\"\"\n    VERBOSE = int(os.getenv('VERBOSE', '1')) != 0\n\n    # Flask-SQLAlchemy options\n    SQLALCHEMY_DATABASE_URI = _get_postgres_uri(POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, 'komidabot_dev')\n\n    # Flask-Session options\n    SESSION_COOKIE_NAME = 'session_dev'\n\n\nclass TestingConfig(BaseConfig):\n    \"\"\"Testing configuration\"\"\"\n    DISABLED = False\n    VERBOSE = False\n    TESTING = True\n    PAGE_ACCESS_TOKEN = None\n    VERIFY_TOKEN = None\n    APP_SECRET = None\n\n    # Flask-SQLAlchemy options\n    SQLALCHEMY_DATABASE_URI = _get_postgres_uri(POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, 'komidabot_test')\n"
  },
  {
    "path": "database/.dockerignore",
    "content": ".dockerignore\nDockerfile\n"
  },
  {
    "path": "database/Dockerfile",
    "content": "# base image\nFROM postgres:11-alpine\n\n# run create.sql on init\nADD create.sql /docker-entrypoint-initdb.d\n"
  },
  {
    "path": "database/create.sql",
    "content": "CREATE DATABASE komidabot_prod;\nCREATE DATABASE komidabot_dev;\nCREATE DATABASE komidabot_test;\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.7'\n\nservices:\n\n  komidabot-db:\n    build:\n      context: ./database\n      dockerfile: Dockerfile\n    restart: on-failure\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_HOST_AUTH_METHOD: trust\n    ports:\n      - \"127.0.0.1:5432:5432\"\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n\n  komidabot-prod:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    restart: \"no\"\n    stop_signal: SIGINT\n    environment:\n      POSTGRES_HOST: komidabot-db\n      POSTGRES_USER: postgres\n      POSTGRES_DB: komidabot_prod\n      APP_SETTINGS: config.ProductionConfig\n    volumes:\n      - prod_sessions:/var/flask_session\n    env_file:\n      - config-prod.env\n    ports:\n      - \"5000:5000\"\n    depends_on:\n      - komidabot-db\n\n  komidabot-dev:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    restart: \"no\"\n    stop_signal: SIGINT\n    environment:\n      POSTGRES_HOST: komidabot-db\n      POSTGRES_USER: postgres\n      POSTGRES_DB: komidabot_dev\n      APP_SETTINGS: config.DevelopmentConfig\n      FLASK_DEBUG: 1\n    volumes:\n      - .:/usr/src/app\n      - dev_sessions:/var/flask_session\n    env_file:\n      - config-dev.env\n    ports:\n      - \"5001:5000\"\n    depends_on:\n      - komidabot-db\n\nvolumes:\n  pgdata:\n    driver: local\n  dev_sessions:\n    driver: local\n  prod_sessions:\n    driver: local\n"
  },
  {
    "path": "entrypoint.sh",
    "content": "#!/usr/bin/env /bin/bash\n\nexport PYTHONDONTWRITEBYTECODE=1\n\n./wait-postgres.sh\n\nif [ $# -eq 0 ]; then\n  trap 'kill -TERM $PID' TERM INT\n  KOMIDABOT_SKIP_INITIALISATION=true flask db upgrade\n  PID=$!\n  wait $PID\n  trap - TERM INT\n  wait $PID\n\n  if [[ \"$FLASK_ENV\" = \"production\" ]]; then\n      exec gunicorn --bind 0.0.0.0:5000 --log-level debug --workers 1 \"app:create_app()\"\n  else\n      exec python3 manage.py run -h 0.0.0.0\n  fi\nelse\n  echo \"Running custom command:\"\n  echo \"$@\"\n  exec /usr/bin/env \"$@\"\nfi\n"
  },
  {
    "path": "extensions.py",
    "content": "from flask_login import LoginManager\nfrom flask_migrate import Migrate\nfrom flask_session import Session\nfrom flask_sqlalchemy import BaseQuery, Model, SQLAlchemy\n\n__all__ = ['session', 'db', 'migrate', 'login', 'ModelBase', 'Table']\n\nsession = Session()\ndb = SQLAlchemy()\nmigrate = Migrate(db=db)\nlogin = LoginManager()\n\n\nclass _ModelBase(Model):\n    query: BaseQuery\n    metadata = None\n\n\nModelBase: _ModelBase = db.Model\nTable = db.Table\n"
  },
  {
    "path": "komidabot/api_utils.py",
    "content": "import json\nimport os\nimport sys\nimport traceback\nfrom functools import wraps\n\nfrom flask import jsonify, request\nfrom jsonschema import ValidationError, Draft7Validator, RefResolver\nfrom werkzeug.exceptions import HTTPException\nfrom werkzeug.http import HTTP_STATUS_CODES\n\nfrom komidabot.app import get_app\nfrom komidabot.debug.state import DebuggableException\n\n__all__ = ['expects_schema', 'wrap_exceptions']\n\n\ndef response_ok():\n    return jsonify({'status': 200, 'message': HTTP_STATUS_CODES[200]}), 200\n\n\ndef response_bad_request():\n    return jsonify({'status': 400, 'message': HTTP_STATUS_CODES[400]}), 200\n\n\ndef response_unauthorized():\n    return jsonify({'status': 401, 'message': HTTP_STATUS_CODES[401]}), 200\n\n\ndef wrap_exceptions(func):\n    @wraps(func)\n    def decorated_func(*args, **kwargs):\n        try:\n            return func(*args, **kwargs)\n        except HTTPException as e:\n            return jsonify({'status': e.code, 'message': HTTP_STATUS_CODES[e.code]}), e.code\n        except DebuggableException as e:\n            app = get_app()\n            app.bot.notify_error(e)\n\n            e.print_info(app.logger)\n\n            return jsonify({'status': 500, 'message': HTTP_STATUS_CODES[500]}), 500\n        except Exception as e:\n            # noinspection PyBroadException\n            try:\n                get_app().bot.notify_error(e)\n            except Exception:\n                pass\n\n            traceback.print_tb(e.__traceback__)\n            print(e, flush=True, file=sys.stderr)\n\n            return jsonify({'status': 500, 'message': HTTP_STATUS_CODES[500]}), 500\n\n    return decorated_func\n\n\ndef expects_schema(input_schema: str = None, output_schema: str = None):\n    in_schema = None\n    if input_schema is not None:\n        input_schema = os.path.join(os.getcwd(), 'schemas', input_schema + '.json')\n        with open(input_schema) as f:\n            in_schema = json.load(f)\n\n        Draft7Validator.check_schema(in_schema)\n        in_resolver = RefResolver(base_uri='file:{}'.format(input_schema), referrer=in_schema)\n        in_validator = Draft7Validator(in_schema, resolver=in_resolver)\n\n    out_schema = None\n    if output_schema is not None:\n        output_schema = os.path.join(os.getcwd(), 'schemas', output_schema + '.json')\n        with open(output_schema) as f:\n            out_schema = json.load(f)\n\n        Draft7Validator.check_schema(out_schema)\n        out_resolver = RefResolver(base_uri='file:{}'.format(output_schema), referrer=out_schema)\n        out_validator = Draft7Validator(out_schema, resolver=out_resolver)\n\n    def decorator(func):\n        @wraps(func)\n        def decorated_func(*args, **kwargs):\n            if in_schema is not None:\n                data = request.get_json(force=False)\n\n                if data is None:\n                    return response_bad_request()\n\n                try:\n                    in_validator.validate(data)\n                except ValidationError:\n                    return response_bad_request()\n\n            output = func(*args, **kwargs)\n\n            if out_schema is not None:\n                response = output[0] if isinstance(output, tuple) else output\n                if response is None or not callable(getattr(response, 'get_data', None)):\n                    raise DebuggableException('Response is probably not a response object')\n\n                out_data = response.get_data()\n\n                try:\n                    out_validator.validate(json.loads(out_data))\n                except ValidationError as e:\n                    raise DebuggableException('Schema validation failed') from e\n\n            return output\n\n        return decorated_func\n\n    return decorator\n"
  },
  {
    "path": "komidabot/app.py",
    "content": "import logging\n\nfrom flask import current_app as _current_app\n\nfrom config import ConfigType\n\n\ndef get_app() -> 'App':\n    return _current_app\n\n\nclass App:\n    def __init__(self, config):\n        import atexit, sys\n        from concurrent.futures import ThreadPoolExecutor as PyThreadPoolExecutor\n\n        from komidabot.facebook.api_interface import ApiInterface\n        from komidabot.facebook.users import UserManager as FBUserManager\n        from komidabot.web.users import UserManager as WebUserManager\n        from komidabot.subscriptions.daily_menu import Channel as DailyMenuChannel\n        from komidabot.subscriptions import SubscriptionManager\n        from komidabot.komidabot import Komidabot\n        from komidabot.translation import GoogleTranslationService, TranslationService\n        from komidabot.users import UnifiedUserManager, UserId, UserManager\n\n        self.logger: logging.Logger\n\n        self.bot_interfaces = dict()  # TODO: Deprecate?\n        self.bot_interfaces['facebook'] = {\n            'api_interface': ApiInterface(config.get('PAGE_ACCESS_TOKEN'))\n        }\n\n        user_manager = UnifiedUserManager()\n        self.user_manager: UserManager = user_manager\n        user_manager.register_manager(FBUserManager())\n        user_manager.register_manager(WebUserManager())\n\n        self.subscription_manager = SubscriptionManager()\n        self.subscription_manager.register_channel(DailyMenuChannel())\n\n        self.translator: TranslationService = GoogleTranslationService()\n\n        self.bot = Komidabot(self)\n\n        # TODO: This could probably also be moved to the Komidabot class\n        self.task_executor = PyThreadPoolExecutor(max_workers=5)\n        atexit.register(PyThreadPoolExecutor.shutdown, self.task_executor)  # Ensure cleanup of resources\n\n        # XXX: Convert from _UserId type in config to the actually used UserId\n        self.admin_ids = [UserId(user.id, user.provider) for user in config.get('ADMIN_IDS', [])]\n\n        with self.app_context():\n            self.user_manager.initialise()\n\n        if not config['TESTING']:\n            self.bot.start_scheduler()\n\n            with self.app_context():\n                from komidabot.models import AppSettings\n                AppSettings.create_entries()\n\n    def app_context(self):\n        raise NotImplementedError()\n\n    @property\n    def config(self) -> 'ConfigType':\n        raise NotImplementedError()\n\n    def _get_current_object(self):\n        raise NotImplementedError\n"
  },
  {
    "path": "komidabot/blueprint.py",
    "content": "import hashlib\nimport hmac\nimport json\nimport pprint\nimport sys\nimport time\nimport traceback\nfrom functools import wraps\n\nfrom flask import Blueprint, abort, escape, request\n\nimport komidabot.facebook.constants as fb_constants\nimport komidabot.facebook.postbacks as postbacks\nimport komidabot.facebook.triggers as triggers\nimport komidabot.localisation as localisation\nimport komidabot.models as models\nimport komidabot.web.constants as web_constants\nfrom extensions import db\nfrom komidabot.app import get_app\nfrom komidabot.debug.state import DebuggableException\nfrom komidabot.facebook.users import User as FacebookUser\nfrom komidabot.komidabot import Bot\nfrom komidabot.messages import TextMessage\nfrom komidabot.users import UserId\nfrom komidabot.web.users import User as WebUser\n\nblueprint = Blueprint('komidabot', __name__)\npp = pprint.PrettyPrinter(indent=2)\n\n\n@blueprint.route('/', methods=['GET'])\ndef handle_facebook_verification():\n    if request.args.get(\"hub.mode\") == \"subscribe\" and request.args.get(\"hub.challenge\"):\n        if request.args.get('hub.verify_token', '') == get_app().config['VERIFY_TOKEN']:\n            print(\"Verified\")\n            return escape(request.args.get('hub.challenge', ''))\n        else:\n            print(\"Wrong token\")\n            return \"Error, wrong validation token\"\n    else:\n        return abort(401)\n\n\ndef validate_signature(func):\n    @wraps(func)\n    def decorated_func(*args, **kwargs):\n        if get_app().config['TESTING']:\n            # Skip validating signature if we're testing\n            return func(*args, **kwargs)\n\n        advertised = request.headers.get(\"X-Hub-Signature\")\n        if advertised is None:\n            return False\n\n        advertised = advertised.replace(\"sha1=\", \"\", 1)\n        data = request.get_data()\n\n        received = hmac.new(\n            key=get_app().config['APP_SECRET'].encode('raw_unicode_escape'),\n            msg=data,\n            digestmod=hashlib.sha1\n        ).hexdigest()\n\n        if hmac.compare_digest(advertised, received):\n            return func(*args, **kwargs)\n\n        return abort(401)\n\n    return decorated_func\n\n\n@blueprint.route('/', methods=['POST'])\n@validate_signature\ndef handle_facebook_webhook():\n    try:\n        app = get_app()\n        data = request.get_json()\n\n        if data and data['object'] == 'page':\n            for entry in data['entry']:\n                entry: dict\n\n                if 'messaging' not in entry:\n                    continue\n\n                for event in entry['messaging']:\n                    sender = event[\"sender\"][\"id\"]\n                    # recipient = event[\"recipient\"][\"id\"]\n\n                    user_manager = app.user_manager\n                    user: FacebookUser = user_manager.get_user(UserId(sender, fb_constants.PROVIDER_ID), event=event)\n\n                    if not isinstance(user, FacebookUser):\n                        # FIXME: Rather have a check that when the user supports \"read\" markers, we mark as read\n                        raise RuntimeError('Expected Facebook User')\n\n                    app.task_executor.submit(_do_handle_facebook_webhook, event, user, app._get_current_object())\n\n            return 'ok', 200\n\n        print(pprint.pformat(data, indent=2), flush=True)\n\n        return abort(400)\n    except DebuggableException as e:\n        app = get_app()\n        app.bot.notify_error(e)\n\n        e.print_info(app.logger)\n    except Exception as e:\n        try:\n            get_app().bot.notify_error(e)\n        except Exception:\n            pass\n\n        traceback.print_tb(e.__traceback__)\n        print(e, flush=True, file=sys.stderr)\n\n        return 'ok', 200\n\n\ndef _do_handle_facebook_webhook(event, user: FacebookUser, app):\n    time.sleep(0.1)  # Yield\n\n    with app.app_context():\n        trigger = triggers.Trigger(aspects=[triggers.SenderAspect(user)])\n\n        needs_commit = False\n\n        if user.get_db_user() is None:\n            trigger.add_aspect(triggers.NewUserAspect())\n            print('Adding new user to the database {}'.format(user.id), flush=True)\n            user.add_to_db()\n            needs_commit = True\n\n        bot: Bot = app.bot\n\n        locale = user.get_locale()\n\n        try:\n            print('Handling message in new path for {}'.format(user.id), flush=True)\n            # print(pprint.pformat(event, indent=2), flush=True)\n\n            if 'message' in event:\n                message = event['message']\n\n                user.mark_message_seen()\n\n                # print(pprint.pformat(message, indent=2), flush=True)\n\n                # TODO: Is this the preferred way to differentiate inputs?\n                #       What about messages that include attachments or other things?\n                # TODO: This now works with aspects rather than inheritance, so in theory this could be done\n                if 'text' in message:\n                    message_text = message['text']\n\n                    trigger = triggers.TextTrigger.extend(trigger, message_text)\n\n                    if '@admin' in message_text:\n                        trigger.add_aspect(triggers.AtAdminAspect())\n\n                    if 'nlp' in message:\n                        if 'detected_locales' in message['nlp'] and len(message['nlp']['detected_locales']) > 0:\n                            # Get the locale that has the highest confidence\n                            locale_entry = max(message['nlp']['detected_locales'], key=lambda x: x['confidence'])\n                            trigger.add_aspect(triggers.LocaleAspect(locale_entry['locale'],\n                                                                     locale_entry['confidence']))\n                            locale = locale_entry['locale']\n\n                        if 'entities' in message['nlp']:\n                            entities = message['nlp']['entities']\n\n                            if 'datetime' in entities:\n                                for entity in entities['datetime']:\n                                    if 'value' in entity:  # Specific date given, vs. date range\n                                        # FIXME: Do we want to add range datetimes?\n                                        trigger.add_aspect(triggers.DatetimeAspect(entity['value'], entity['grain']))\n\n                    if user.is_admin() and message_text == 'sub':\n                        # Simulate subscription instead\n                        trigger = triggers.SubscriptionTrigger.extend(trigger)\n\n                if app.config.get('DISABLED'):\n                    if not user.is_admin():\n                        if triggers.AtAdminAspect not in trigger:\n                            user.send_message(TextMessage(trigger, localisation.DOWN_FOR_MAINTENANCE(locale)))\n\n                        return\n\n                    # sender_obj.send_text_message('Note: The bot is currently disabled')\n\n            elif 'postback' in event:\n                # print(pprint.pformat(event, indent=2), flush=True)\n\n                user.mark_message_seen()\n\n                if app.config.get('DISABLED'):\n                    if not user.is_admin():\n                        if triggers.AtAdminAspect not in trigger:\n                            user.send_message(TextMessage(trigger, localisation.DOWN_FOR_MAINTENANCE(locale)))\n\n                        return\n\n                postback: dict = event['postback']\n\n                payload = postback.get('payload')\n\n                try:\n                    data: dict = json.loads(payload)\n                except json.JSONDecodeError:\n                    raise\n\n                trigger = triggers.PostbackTrigger.extend(trigger, data['name'], data['args'], data['kwargs'])\n\n                # TODO: This will be cleaner if we work with intents (see komidabot.py)\n                postback_obj = postbacks.lookup_postback(trigger.name)\n\n                if postback_obj:\n                    trigger = postback_obj.call_postback(trigger, *trigger.args, **trigger.kwargs)\n\n                    if trigger is None:\n                        return  # Indicates the trigger was processed\n                        # TODO: Again, this will be cleaner if we work with intents (see komidabot.py)\n                else:\n                    get_app().bot.message_admins(TextMessage(triggers.Trigger(), 'Unknown postback type received!'))\n                    user.send_message(TextMessage(trigger, localisation.ERROR_POSTBACK(locale)))\n                    return\n            elif 'request_thread_control' in event:\n                request_thread_control: dict = event['request_thread_control']\n\n                requested_owner_app_id = request_thread_control['requested_owner_app_id']\n                metadata = request_thread_control['metadata']\n                if requested_owner_app_id == 263902037430900:  # Page Inbox app id\n                    # We'll allow the request\n                    app.bot_interfaces['facebook']['api_interface'].post_pass_thread_control({\n                        'recipient': {'id': user.id.id},\n                        'target_app_id': requested_owner_app_id,\n                        'metadata': metadata\n                    })\n\n                return\n            elif 'pass_thread_control' in event:\n                return  # Right now we don't need to handle this one\n            else:\n                print(pprint.pformat(event, indent=2), flush=True)\n\n                get_app().bot.message_admins(TextMessage(triggers.Trigger(), 'Unknown message type received!'))\n\n                return\n\n            bot.trigger_received(trigger)\n\n            if needs_commit:\n                db.session.commit()\n        except DebuggableException as e:\n            app = get_app()\n            app.bot.notify_error(e)\n\n            e.print_info(app.logger)\n        except Exception as e:\n            try:\n                app.logger.error('Error while handling event:\\n{}'.format(pprint.pformat(event, indent=2)))\n                get_app().bot.notify_error(e)\n            except Exception:\n                pass\n\n            user.send_message(TextMessage(trigger, localisation.INTERNAL_ERROR(locale)))\n            app.logger.exception(e)\n\n\n@blueprint.route('/subscription', methods=['POST'])\ndef handle_web_push_subscription():\n    try:\n        app = get_app()\n        data = request.get_json()\n\n        print(pprint.pformat(data, indent=2), flush=True)\n\n        if data and 'subscription' in data:\n            subscription = data['subscription']\n\n            if 'endpoint' not in subscription:\n                return abort(400)\n\n            if 'keys' not in subscription:\n                return abort(400)\n\n            endpoint = subscription['endpoint']\n            keys = subscription['keys']\n\n            needs_commit = False\n\n            user_manager = app.user_manager\n            user: WebUser = user_manager.get_user(UserId(endpoint, web_constants.PROVIDER_ID))\n\n            if user.get_db_user() is None:\n                print('Adding new subscription to the database {}'.format(user.id), flush=True)\n                user.add_to_db()\n                user.set_data({\n                    'keys': keys\n                })\n                needs_commit = True\n\n            if 'days' in subscription:\n                days = subscription['days']\n\n                if len(days) != 5:\n                    return abort(400)\n\n                for i in range(5):\n                    if i >= 5:\n                        break\n\n                    day = models.week_days[i]\n                    campus_id = days[i]\n\n                    campus = user.get_campus_for_day(day)\n\n                    if campus_id is None:\n                        if user.disable_subscription_for_day(day):\n                            needs_commit = True\n                    elif campus is None or campus.id != campus_id:\n                        campus = models.Campus.get_by_id(campus_id)\n                        if campus is None:\n                            continue\n                        user.set_campus_for_day(campus, day)\n                        needs_commit = True\n\n            if needs_commit:\n                db.session.commit()\n\n            return '{}', 200\n\n        return abort(400)\n    except DebuggableException as e:\n        app = get_app()\n        app.bot.notify_error(e)\n\n        e.print_info(app.logger)\n\n        return abort(500)\n    except Exception as e:\n        try:\n            get_app().bot.notify_error(e)\n        except Exception:\n            pass\n\n        traceback.print_tb(e.__traceback__)\n        print(e, flush=True, file=sys.stderr)\n\n        return abort(500)\n"
  },
  {
    "path": "komidabot/blueprint_api.py",
    "content": "import json\nfrom datetime import date, timedelta\nfrom typing import Any, Dict, TypedDict, Union\n\nfrom flask import Blueprint, abort, jsonify, request\nfrom flask_login import current_user, login_required, UserMixin\nfrom werkzeug.http import HTTP_STATUS_CODES\n\nimport komidabot.api_utils as api_utils\nimport komidabot.messages as messages\nimport komidabot.models as models\nimport komidabot.triggers as triggers\nimport komidabot.web.constants as web_constants\nfrom extensions import db, login\nfrom komidabot.app import get_app\nfrom komidabot.debug.administration import notify_admins\nfrom komidabot.models_training import LearningDatapoint\nfrom komidabot.models_users import RegisteredUser\nfrom komidabot.users import UserId\nfrom komidabot.web.users import User as WebUser\n\nblueprint = Blueprint('komidabot api', __name__)\ncurrent_user: 'Union[RegisteredUser, UserMixin]'\n\n\ndef translatable_to_object(translatable: models.Translatable):\n    result = {}\n    for translation in translatable.translations:\n        result[translation.language] = translation.translation\n\n    return result\n\n\n@blueprint.route('/subscribe', methods=['POST'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(input_schema='POST_api_subscribe', output_schema='api_response_strict')\ndef post_subscribe():\n    class PostData(TypedDict):\n        endpoint: str\n        keys: Dict[str, str]\n        channel: str\n        data: Any\n\n    if not current_user.is_role('admin'):\n        return api_utils.response_unauthorized()\n\n    post_data: PostData = request.get_json()\n    endpoint = post_data['endpoint']\n    keys = post_data['keys']\n    channel = post_data['channel']\n    data = post_data['data'] if 'data' in post_data else None\n\n    if channel == 'administration':\n        if not current_user.is_authenticated:\n            return login.unauthorized()\n\n        current_user.add_subscription(endpoint, keys)\n\n        db.session.commit()\n\n        return api_utils.response_ok()\n    else:\n        # FIXME: This code is not really done, but until we can send out daily menus in a consistent manner it'll\n        #        have to be this way\n\n        return api_utils.response_bad_request()\n        # app = get_app()\n        # user: WebUser = app.user_manager.get_user(UserId(endpoint, web_constants.PROVIDER_ID))\n        #\n        # if user.get_db_user() is None:\n        #     user.add_to_db()\n        #     user.set_data({\n        #         'keys': keys\n        #     })\n        #\n        # # if not channel.user_supported(user):\n        # #     return api_utils.response_bad_request()\n        #\n        # if app.subscription_manager.user_subscribe(user, channel, data=data):\n        #     return api_utils.response_ok()\n        # else:\n        #     return api_utils.response_bad_request()\n\n\n@blueprint.route('/subscribe', methods=['DELETE'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(input_schema='DELETE_api_subscribe', output_schema='api_response_strict')\ndef delete_subscribe():\n    class PostData(TypedDict):\n        endpoint: str\n        channel: str\n\n    if not current_user.is_role('admin'):\n        return api_utils.response_unauthorized()\n\n    post_data: PostData = request.get_json()\n    endpoint = post_data['endpoint']\n    channel = post_data['channel']\n\n    if channel == 'administration':\n        if not current_user.is_authenticated:\n            return login.unauthorized()\n\n        current_user.remove_subscription(endpoint)\n\n        db.session.commit()\n\n        return api_utils.response_ok()\n    else:\n        # FIXME: This code is not really done, but until we can send out daily menus in a consistent manner it'll\n        #        have to be this way\n\n        return api_utils.response_bad_request()\n        # app = get_app()\n        # user: WebUser = app.user_manager.get_user(UserId(endpoint, web_constants.PROVIDER_ID))\n        #\n        # if user.get_db_user() is None:\n        #     return api_utils.response_ok()\n        #\n        # if app.subscription_manager.user_unsubscribe(user, channel):\n        #     return api_utils.response_ok()\n        # else:\n        #     return api_utils.response_bad_request()\n\n\n@blueprint.route('/subscribe', methods=['PUT'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(input_schema='PUT_api_subscribe', output_schema='api_response_strict')\ndef put_subscribe():\n    class PostData(TypedDict):\n        old_endpoint: str\n        endpoint: str\n        keys: Dict[str, str]\n\n    if not current_user.is_role('admin'):\n        return api_utils.response_unauthorized()\n\n    post_data: PostData = request.get_json()\n    old_endpoint = post_data['old_endpoint']\n    endpoint = post_data['endpoint']\n    keys = post_data['keys']\n\n    app = get_app()\n    user: WebUser = app.user_manager.get_user(UserId(old_endpoint, web_constants.PROVIDER_ID))\n\n    # FIXME: Change internal ID of user and keys\n    # FIXME: Change admin subscriptions as well? Need to verify this\n\n    # FIXME: This code is not really done, but until we can send out daily menus in a consistent manner it'll\n    #        have to be this way\n\n    return api_utils.response_bad_request()\n\n\n@blueprint.route('/trigger', methods=['POST'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(input_schema='POST_api_trigger', output_schema='api_response_strict')\n@login_required\ndef post_trigger():\n    class PostData(TypedDict):\n        trigger: str\n\n    if not current_user.is_role('admin'):\n        return api_utils.response_unauthorized()\n\n    post_data: PostData = request.get_json()\n    trigger = post_data['trigger']\n\n    if trigger == 'notification_test_error':\n        try:\n            raise RuntimeError('Test exception')\n        except RuntimeError as e:\n            notify_admins(messages.ExceptionMessage(triggers.Trigger(), e))\n\n        return api_utils.response_ok()\n    elif trigger == 'notification_test_text':\n        notify_admins(messages.TextMessage(triggers.Trigger(), 'Test notification'))\n\n        return api_utils.response_ok()\n    elif trigger == 'menu_update':\n        from komidabot.komidabot import update_menus\n\n        update_menus()\n\n        return api_utils.response_ok()\n    else:\n        return api_utils.response_bad_request()\n\n\n@blueprint.route('/learning', methods=['GET'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(output_schema='GET_api_learning.response')\n@login_required\ndef get_learning():\n    if not current_user.is_role('learner'):\n        return api_utils.response_unauthorized()\n\n    datapoint = LearningDatapoint.get_random(current_user)\n\n    if datapoint is None:\n        return jsonify({'status': 200, 'message': HTTP_STATUS_CODES[200], 'data': None}), 200\n\n    processed = json.loads(datapoint.processed_data)\n\n    result = {\n        'id': str(datapoint.id),\n        'screenshot': datapoint.screenshot,\n        'course_name': processed['name']['nl'],\n        'course_type': models.CourseType[processed['course_type']].value,\n        'course_sub_type': models.CourseSubType[processed['course_sub_type']].value,\n        'price_students': processed['price_students'],\n        'price_staff': processed['price_staff'],\n    }\n\n    return jsonify({'status': 200, 'message': HTTP_STATUS_CODES[200], 'data': result}), 200\n\n\n@blueprint.route('/learning', methods=['POST'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(input_schema='POST_api_learning', output_schema='api_response_strict')\n@login_required\ndef post_learning():\n    class PostData(TypedDict):\n        id: str\n        course_name_correct: bool\n        course_type: int\n        course_sub_type: int\n        price_students_correct: bool\n        price_staff_correct: bool\n\n    if not current_user.is_role('learner'):\n        return api_utils.response_unauthorized()\n\n    post_data: PostData = request.get_json()\n\n    datapoint = LearningDatapoint.find_by_id(int(post_data['id']))\n\n    datapoint.user_submit(current_user, {\n        'course_name_correct': post_data['course_name_correct'],\n        'course_type': post_data['course_type'],\n        'course_sub_type': post_data['course_sub_type'],\n        'price_students_correct': post_data['price_students_correct'],\n        'price_staff_correct': post_data['price_staff_correct']\n    })\n\n    db.session.commit()\n\n    return jsonify({'status': 200, 'message': HTTP_STATUS_CODES[200]}), 200\n\n\n@blueprint.route('/campus', methods=['GET'])\n# TODO: @api_utils.wrap_exceptions\n# TODO: @api_utils.expects_schema\ndef get_campus_list():\n    \"\"\"\n    Gets a list of all available campuses.\n    \"\"\"\n\n    result = []\n\n    campuses = models.Campus.get_all_active()\n\n    for campus in campuses:\n        result.append({\n            'id': campus.id,\n            'name': campus.name,\n            'short_name': campus.short_name,\n            # TODO: Needs opening hours\n        })\n\n    return jsonify(result)\n\n\n@blueprint.route('/campus/closing_days/<week_str>', methods=['GET'], defaults={'short_name': None})\n@blueprint.route('/campus/<short_name>/closing_days/<week_str>', methods=['GET'])\n# TODO: @api_utils.wrap_exceptions\n# TODO: @api_utils.expects_schema\ndef get_active_closing_days(short_name: str, week_str: str):\n    \"\"\"\n    Gets all currently active closures.\n    \"\"\"\n\n    if short_name is None:\n        campuses = models.Campus.get_all_active()\n    else:\n        campus = models.Campus.get_by_short_name(short_name)\n\n        if campus is None:\n            return abort(400)\n\n        campuses = [models.Campus.get_by_short_name(short_name)]\n\n    try:\n        week_day = date.fromisoformat(week_str)\n    except ValueError:\n        return abort(400)\n\n    week_start = week_day + timedelta(days=-week_day.weekday())  # Start on Monday\n\n    result = {}\n\n    for campus in campuses:\n        current_campus = result[campus.short_name] = []\n\n        for i in range(5):\n            closed_data = models.ClosingDays.find_is_closed(campus, week_start + timedelta(days=i))\n\n            if closed_data is not None:\n                current_campus.append({\n                    'first_day': closed_data.first_day.isoformat(),\n                    'last_day': closed_data.last_day.isoformat() if closed_data.last_day is not None else None,\n                    'reason': translatable_to_object(closed_data.translatable),\n                })\n            else:\n                current_campus.append(None)\n\n    return jsonify(result)\n\n\n@blueprint.route('/campus/<short_name>/menu/<day_str>', methods=['GET'])\n# TODO: @api_utils.wrap_exceptions\n# TODO: @api_utils.expects_schema\ndef get_menu(short_name: str, day_str: str):\n    \"\"\"\n    Gets the menu for a specific campus on a day.\n    \"\"\"\n    campus = models.Campus.get_by_short_name(short_name)\n\n    if campus is None:\n        return abort(400)\n\n    try:\n        day_date = date.fromisoformat(day_str)\n    except ValueError:\n        return abort(400)\n\n    menu = models.Menu.get_menu(campus, day_date)\n\n    result = []\n\n    if menu is None:\n        return jsonify(result)\n\n    for menu_item in menu.menu_items:\n        menu_item: models.MenuItem\n        value = {\n            'course_type': menu_item.course_type.value,\n            'course_sub_type': menu_item.course_sub_type.value,\n            'translation': translatable_to_object(menu_item.translatable),\n        }\n        if menu_item.price_students:\n            value['price_students'] = str(models.MenuItem.format_price(menu_item.price_students))\n        if menu_item.price_staff:\n            value['price_staff'] = str(models.MenuItem.format_price(menu_item.price_staff))\n        result.append(value)\n\n    return jsonify(result)\n"
  },
  {
    "path": "komidabot/blueprint_authentication.py",
    "content": "import json\nfrom typing import Optional, Union\nfrom urllib.parse import urlparse, quote, unquote\n\nimport requests\nfrom flask import abort, Blueprint, jsonify, redirect, request, url_for\nfrom flask_login import current_user, login_required, login_user, logout_user, UserMixin\nfrom oauthlib.oauth2 import InvalidGrantError, OAuth2Error, WebApplicationClient\nfrom werkzeug.http import HTTP_STATUS_CODES\n\nimport komidabot.api_utils as api_utils\nimport komidabot.config as app_config\nfrom extensions import db, login\nfrom komidabot.app import App, get_app\nfrom komidabot.models_users import RegisteredUser\n\nblueprint = Blueprint('komidabot authentication', __name__)\ncurrent_user: 'Union[RegisteredUser, UserMixin]'\n\ngoogle_client: Optional[Union[WebApplicationClient, bool]] = None\ngoogle_provider_config = None\n\n\ndef init_google_client(app: App):\n    global google_client\n    client_id = app.config.get('AUTH_GOOGLE_CLIENT_ID')\n    if client_id:\n        google_client = WebApplicationClient(client_id)\n    else:\n        google_client = False\n\n\ndef get_google_provider_cfg():\n    global google_provider_config\n    if google_provider_config is None:\n        google_provider_config = requests.get('https://accounts.google.com/.well-known/openid-configuration').json()\n    return google_provider_config\n\n\n@login.user_loader\ndef user_loader(user_id):\n    return RegisteredUser.get_by_id(user_id)\n\n\n@login.unauthorized_handler\ndef unauthorized_handler():\n    return api_utils.response_unauthorized()\n\n\n@blueprint.route('/login', methods=['GET'])\n@api_utils.wrap_exceptions\ndef get_login():\n    next_url = request.args.get('next', None)\n    return redirect(url_for('.get_login_google', next=next_url))\n\n\n@blueprint.route('/login/google', methods=['GET'])\n@api_utils.wrap_exceptions\ndef get_login_google():\n    app = get_app()\n\n    if google_client is None:\n        init_google_client(app)\n\n    if google_client is False:\n        return redirect('/login/not_available')\n\n    google_provider_cfg = get_google_provider_cfg()\n\n    authorization_endpoint = google_provider_cfg['authorization_endpoint']\n\n    state = {}\n\n    if 'next' in request.args:\n        next_url = request.args.get('next')\n        parsed_next_url = urlparse(next_url)\n\n        # Prevent changing the scheme or host\n        if parsed_next_url.scheme != '' or parsed_next_url.netloc != '':\n            return abort(400)\n\n        state['next'] = parsed_next_url.geturl()\n\n    request_uri = google_client.prepare_request_uri(\n        authorization_endpoint,\n        redirect_uri=url_for('.get_login_google_callback', _external=True),\n        scope=['openid', 'email', 'profile'],\n        state=quote(json.dumps(state))\n    )\n    return redirect(request_uri)\n\n\n@blueprint.route('/login/google/callback', methods=['GET'])\n@api_utils.wrap_exceptions\ndef get_login_google_callback():\n    app = get_app()\n\n    if google_client is None:\n        init_google_client(app)\n\n    if google_client is False:\n        return redirect('/login/not_available')\n\n    code = request.args.get('code')\n    state = json.loads(unquote(request.args.get('state')))\n    next_url = state.get('next', '/')\n\n    google_provider_cfg = get_google_provider_cfg()\n\n    token_endpoint = google_provider_cfg['token_endpoint']\n    token_url, headers, body = google_client.prepare_token_request(\n        token_endpoint,\n        authorization_response=request.url,\n        redirect_url=request.base_url,\n        code=code\n    )\n    token_response = requests.post(\n        token_url,\n        headers=headers,\n        data=body,\n        auth=(app.config.get('AUTH_GOOGLE_CLIENT_ID'), app.config.get('AUTH_GOOGLE_CLIENT_SECRET')),\n    )\n\n    try:\n        google_client.parse_request_body_response(json.dumps(token_response.json()))\n    except InvalidGrantError:\n        # Invalid grant, let's try the login flow again\n        if next_url != '/':\n            return redirect(next_url)\n        return redirect('/login/internal_error')\n    except OAuth2Error:\n        return redirect('/login/internal_error')\n\n    userinfo_endpoint = google_provider_cfg['userinfo_endpoint']\n    uri, headers, body = google_client.add_token(userinfo_endpoint)\n    userinfo_response = requests.get(uri, headers=headers, data=body)\n\n    # You want to make sure their email is verified.\n    # The user authenticated with Google, authorized your\n    # app, and now you've verified their email through Google!\n    if userinfo_response.json().get('email_verified'):\n        unique_id = userinfo_response.json()['sub']\n        users_email = userinfo_response.json()['email']\n        picture = userinfo_response.json()['picture']\n        users_name = userinfo_response.json()['given_name']\n    else:\n        return redirect('/login/not_verified')\n\n    user = RegisteredUser.find_by_provider_id('google', unique_id)\n    if not user:\n        if app_config.is_registrations_enabled():\n            user = RegisteredUser.create('google', unique_id, users_name, users_email, picture)\n            db.session.commit()\n        else:\n            return redirect('/login/login_closed')\n\n    if not user.is_active:\n        return redirect('/login/not_active')\n\n    login_user(user)\n\n    return redirect(next_url)\n\n\n@blueprint.route('/logout', methods=['GET'])\n@login_required\ndef get_logout():\n    logout_user()\n\n    if 'next' in request.args:\n        next_url = request.args.get('next')\n        parsed_next_url = urlparse(next_url)\n\n        # Prevent changing the scheme or host\n        if parsed_next_url.scheme == '' and parsed_next_url.netloc == '':\n            return redirect(parsed_next_url.geturl())\n\n    return redirect('/')\n\n\n@blueprint.route('/authorized', methods=['GET'])\n@api_utils.wrap_exceptions\n@api_utils.expects_schema(output_schema='GET_api_authorized.response')\n@login_required\ndef get_authorized():\n    roles = [role.name for role in current_user.get_roles()]\n\n    return jsonify({'status': 200, 'message': HTTP_STATUS_CODES[200], 'roles': roles}), 200\n\n# TODO: Add /users endpoint to manage users as admin\n"
  },
  {
    "path": "komidabot/bot.py",
    "content": "from komidabot.messages import Trigger\n\n\nclass Bot:\n    def trigger_received(self, trigger: Trigger):\n        raise NotImplementedError()\n\n    # TODO: This should probably be a trigger instead\n    def notify_error(self, error: Exception):\n        raise NotImplementedError()\n"
  },
  {
    "path": "komidabot/config.py",
    "content": "from komidabot.models import AppSettings\n\n\ndef is_registrations_enabled():\n    return AppSettings.get_value('registrations_enabled') is True\n"
  },
  {
    "path": "komidabot/debug/administration.py",
    "content": "import copy\nimport json\nfrom typing import Any, Callable, NoReturn\n\nfrom pywebpush import webpush, WebPushException\n\nimport komidabot.messages as messages\nfrom komidabot.app import get_app\nfrom komidabot.models_users import AdminSubscription, RegisteredUser\n\nVAPID_CLAIMS = {\n    'sub': 'mailto:komidabot@gmail.com'\n}\n\n\ndef notify_admins(message: messages.Message):\n    target: Callable[[AdminSubscription, Any], NoReturn]\n\n    if isinstance(message, messages.TextMessage):\n        target = _send_text_message\n    elif isinstance(message, messages.ExceptionMessage):\n        target = _send_exception_message\n    else:\n        raise ValueError('Unsupported message type')\n\n    for user in RegisteredUser.get_all():\n        for sub in user.get_subscriptions():\n            message_result = target(sub, message)\n\n            if message_result == messages.MessageSendResult.GONE:\n                # Gone = User no longer exists, delete from database\n                user.remove_subscription(sub['endpoint'])\n\n\ndef _send_notification(subscription: AdminSubscription, data) -> messages.MessageSendResult:\n    app = get_app()\n\n    try:\n        response = webpush(\n            subscription_info=subscription,\n            data=json.dumps(data),\n            vapid_private_key=app.config['VAPID_PRIVATE_KEY'],\n            vapid_claims=copy.deepcopy(VAPID_CLAIMS)\n        )\n\n        if app.config.get('VERBOSE'):\n            print('Received {} for push {}'.format(response.status_code, subscription['endpoint']), flush=True)\n            print(response.content, flush=True)\n\n        return messages.MessageSendResult.SUCCESS\n    except WebPushException as e:\n        response = e.response\n\n        if app.config.get('VERBOSE'):\n            print('Received {} for push {}'.format(response.status_code, subscription['endpoint']), flush=True)\n            print(response.content, flush=True)\n\n        if 500 <= response.status_code < 600:\n            return messages.MessageSendResult.EXTERNAL_ERROR\n\n        if response.status_code == 429:  # Too many requests, rate limited\n            pass  # TODO: Handle rate-limiting\n        if response.status_code == 400:  # Invalid request\n            return messages.MessageSendResult.ERROR\n        if response.status_code == 404:  # Subscription not found\n            return messages.MessageSendResult.GONE\n        if response.status_code == 410:  # Subscription has been removed\n            return messages.MessageSendResult.GONE\n        if response.status_code == 413:  # Payload too large\n            return messages.MessageSendResult.ERROR\n\n        return messages.MessageSendResult.ERROR\n\n\ndef _send_text_message(subscription: AdminSubscription,\n                       message: messages.TextMessage) -> messages.MessageSendResult:\n    data = {\n        'notification': {\n            # 'lang': 'NL',\n            'badge': 'https://komidabot.xyz/assets/icons/notification-badge-android-72x72.png',\n            'title': 'Komidabot message',\n            'body': message.text,\n            'vibrate': [],\n            'renotify': False,\n            'requireInteraction': False,\n            'actions': [],\n            'silent': False,\n        }\n    }\n\n    return _send_notification(copy.deepcopy(subscription), data)\n\n\ndef _send_exception_message(subscription: AdminSubscription,\n                            message: messages.ExceptionMessage) -> messages.MessageSendResult:\n    exception_string = str(message.source)\n\n    if exception_string:\n        body = '{}: {}'.format(type(message.source).__name__, exception_string)\n    else:\n        body = type(message.source).__name__\n\n    data = {\n        'notification': {\n            # 'lang': 'NL',\n            'badge': 'https://komidabot.xyz/assets/icons/notification-badge-android-72x72.png',\n            'title': 'Komidabot: Exception',\n            'body': body,\n            'vibrate': [],\n            'renotify': False,\n            'requireInteraction': False,\n            'actions': [],\n            'silent': False,\n        }\n    }\n\n    return _send_notification(copy.deepcopy(subscription), data)\n"
  },
  {
    "path": "komidabot/debug/state.py",
    "content": "from logging import Logger\nfrom typing import Any, List, Optional\n\n\nclass ProgramStateTrace:\n    def __init__(self):\n        self._root: 'ProgramState' = InitialProgramState()\n        self._current: 'ProgramState' = self._root\n\n    def state(self, state: 'ProgramState'):\n        return WithProgramState(self, state)\n\n    def push(self, state: 'ProgramState'):\n        assert state is not None\n\n        state.parent = self._current\n        self._current.children.append(state)\n        self._current = state\n\n    def pop(self):\n        assert self._current.parent is not None\n\n        self._current = self._current.parent\n\n    def prepend(self, parent: 'ProgramStateTrace'):\n        # Add current tree as child of prepended tree\n        parent._current.children.append(self._root)\n        # And update our old root's parent accordingly\n        self._root.parent = parent._current\n        # Then set the new root to the prepended tree's root\n        self._root = parent._root\n\n    def append(self, child: 'ProgramStateTrace'):\n        # Add child tree as child to current node\n        self._current.children.append(child._root)\n        # And set the child's parent accordingly\n        child._root.parent = self._current\n\n    def get_state(self) -> 'ProgramState':\n        return self._current\n\n    def __repr__(self):\n        result = []\n        current = self._current\n        while current is not None:\n            result.insert(0, '- ' + repr(current))\n            current = current.parent\n\n        return '\\n'.join(['Program state trace:'] + result)\n\n\nclass ProgramState:\n    def __init__(self):\n        self.parent: 'Optional[ProgramState]' = None\n        self.children: 'List[ProgramState]' = []\n\n\nclass InitialProgramState(ProgramState):\n    def __repr__(self):\n        return 'InitialState'\n\n\nclass SimpleProgramState(ProgramState):\n    def __init__(self, name: str, data: Any = None):\n        super().__init__()\n        self.name = name\n        self.data = data\n\n    def __repr__(self):\n        return 'State({}, {})'.format(repr(self.name), repr(self.data))\n\n\nclass DebuggableException(Exception):\n    def __init__(self, message: str, trace: ProgramStateTrace = None):\n        super().__init__(message)\n        self._trace = trace\n\n    def get_trace(self) -> ProgramStateTrace:\n        return self._trace\n\n    def get_or_set_trace(self, trace: ProgramStateTrace) -> ProgramStateTrace:\n        if self._trace is None:\n            self._trace = trace\n        return self._trace\n\n    def get_state(self) -> ProgramState:\n        return self._trace.get_state()\n\n    def print_info(self, logger: Logger):\n        logger.error('Error trace: {}'.format(self.get_trace()))\n        # Redundant log statement:\n        # logger.error('Error last state: {}'.format(self.get_state()))\n        logger.exception(self)\n\n\nclass WithProgramState:\n    def __init__(self, trace: ProgramStateTrace, state: ProgramState):\n        self._trace = trace\n        self._state = state\n\n    def __enter__(self):\n        self._trace.push(self._state)\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if exc_val is not None:\n            if isinstance(exc_val, DebuggableException):\n                trace = exc_val.get_or_set_trace(self._trace)\n                if trace is not self._trace:\n                    trace.prepend(self._trace)\n            else:\n                raise DebuggableException('Unspecified error', self._trace) from exc_val\n        else:\n            self._trace.pop()\n"
  },
  {
    "path": "komidabot/external_menu.py",
    "content": "import atexit\nimport datetime\nimport json\nimport re\nfrom decimal import Decimal\nfrom typing import Any, Dict, Optional, Union\n\nimport requests\n\nimport komidabot.models as models\nfrom extensions import db\nfrom komidabot.debug.state import DebuggableException, ProgramStateTrace, SimpleProgramState\nfrom komidabot.rate_limit import Limiter\nfrom komidabot.translation import LANGUAGE_DUTCH\n\nBASE_ENDPOINT = 'https://restickets.uantwerpen.be/'\nMENU_API = '{endpoint}api/GetMenuByDate/{campus}/{date}'\nPRICE_API = '{endpoint}api/getPriceConversion/{price}'\nALL_MENU_API = '{endpoint}api/GetMenu/{date}'\n\nAPI_GET_HEADERS = dict()\nAPI_GET_HEADERS['Accept'] = 'application/json'\n\nCOURSE_LOGOS_RAW = [\n    {\"id\": 201, \"nameNl\": \"bio\", \"nameEn\": \"bio\", \"logo\": \"ikoon-bio.gif\", \"sortorder\": 2},\n    {\"id\": 202, \"nameNl\": \"gevogelte\", \"nameEn\": \"poultry\", \"logo\": \"ikoon-gevogelte.gif\", \"sortorder\": 3},\n    {\"id\": 203, \"nameNl\": \"grill\", \"nameEn\": \"grill\", \"logo\": \"ikoon-grill.gif\", \"sortorder\": 1},\n    {\"id\": 204, \"nameNl\": \"kaas\", \"nameEn\": \"cheese\", \"logo\": \"ikoon-kaas.gif\", \"sortorder\": 3},\n    {\"id\": 205, \"nameNl\": \"konijn\", \"nameEn\": \"rabbit\", \"logo\": \"ikoon-konijn.gif\", \"sortorder\": 3},\n    {\"id\": 206, \"nameNl\": \"lam\", \"nameEn\": \"lamb\", \"logo\": \"ikoon-lam.gif\", \"sortorder\": 3},\n    {\"id\": 207, \"nameNl\": \"pasta\", \"nameEn\": \"pasta\", \"logo\": \"ikoon-pasta.gif\", \"sortorder\": 1},\n    {\"id\": 208, \"nameNl\": \"rund\", \"nameEn\": \"ox\", \"logo\": \"ikoon-rund.gif\", \"sortorder\": 3},\n    {\"id\": 209, \"nameNl\": \"salade\", \"nameEn\": \"salad\", \"logo\": \"ikoon-salade.gif\", \"sortorder\": 1},\n    {\"id\": 210, \"nameNl\": \"snack\", \"nameEn\": \"snack\", \"logo\": \"ikoon-snack.gif\", \"sortorder\": 1},\n    {\"id\": 211, \"nameNl\": \"soep\", \"nameEn\": \"soup\", \"logo\": \"ikoon-soep.gif\", \"sortorder\": 1},\n    {\"id\": 212, \"nameNl\": \"varken\", \"nameEn\": \"pig\", \"logo\": \"ikoon-varken.gif\", \"sortorder\": 3},\n    {\"id\": 213, \"nameNl\": \"vegan\", \"nameEn\": \"vegan\", \"logo\": \"ikoon-vegan.gif\", \"sortorder\": 2},\n    {\"id\": 214, \"nameNl\": \"veggie\", \"nameEn\": \"veggie\", \"logo\": \"ikoon-veggie.gif\", \"sortorder\": 2},\n    {\"id\": 215, \"nameNl\": \"vis\", \"nameEn\": \"fish\", \"logo\": \"ikoon-vis.gif\", \"sortorder\": 3},\n    {\"id\": 216, \"nameNl\": \"less meat\", \"nameEn\": \"less meat\", \"logo\": \"ikoon-less.gif\", \"sortorder\": 1},\n    {\"id\": 217, \"nameNl\": \"healthify\", \"nameEn\": \"healthify\", \"logo\": \"healthify.gif\", \"sortorder\": 1},\n    {\"id\": 218, \"nameNl\": \"bruin broodje\", \"nameEn\": \"brown bread\", \"logo\": \"ikoon-bruin-broodje.gif\", \"sortorder\": 1},\n    {\"id\": 219, \"nameNl\": \"wit broodje\", \"nameEn\": \"white bread\", \"logo\": \"ikoon-wit-broodje.gif\", \"sortorder\": 1},\n    {\"id\": 220, \"nameNl\": \"conceptbroodje\", \"nameEn\": \"concept bread\", \"logo\": \"ikoon-concept-broodje.gif\",\n     \"sortorder\": 1}\n]\n\nCOURSE_ALLERGENS_RAW = [\n    {\"id\": 200, \"nameNl\": \"Ei\", \"nameEn\": \"Egg\", \"logo\": \"Ei.gif\"},\n    {\"id\": 201, \"nameNl\": \"Gluten-tarwe\", \"nameEn\": \"Wheat gluten\", \"logo\": \"Gluten-tarwe.gif\"},\n    {\"id\": 202, \"nameNl\": \"Lupine\", \"nameEn\": \"Lupine\", \"logo\": \"Lupine.gif\"},\n    {\"id\": 203, \"nameNl\": \"Melk-lactose\", \"nameEn\": \"Milk lactose\", \"logo\": \"Melk-lactose.gif\"},\n    {\"id\": 204, \"nameNl\": \"Mosterd\", \"nameEn\": \"Mustard\", \"logo\": \"Mosterd.gif\"},\n    {\"id\": 205, \"nameNl\": \"Noten\", \"nameEn\": \"nuts\", \"logo\": \"Noten.gif\"},\n    {\"id\": 206, \"nameNl\": \"Pinda\", \"nameEn\": \"Peanut\", \"logo\": \"Pinda.gif\"},\n    {\"id\": 207, \"nameNl\": \"Schaaldieren\", \"nameEn\": \"shellfish\", \"logo\": \"Schaaldieren.gif\"},\n    {\"id\": 208, \"nameNl\": \"Selderij\", \"nameEn\": \"Celery\", \"logo\": \"Selderij.gif\"},\n    {\"id\": 209, \"nameNl\": \"Sesam\", \"nameEn\": \"Sesame\", \"logo\": \"Sesam.gif\"},\n    {\"id\": 210, \"nameNl\": \"Soja\", \"nameEn\": \"soya\", \"logo\": \"Soja.gif\"},\n    {\"id\": 211, \"nameNl\": \"Sulfiet\", \"nameEn\": \"sulfite\", \"logo\": \"Sulfiet.gif\"},\n    {\"id\": 212, \"nameNl\": \"Vis\", \"nameEn\": \"Fish\", \"logo\": \"Vis.gif\"},\n    {\"id\": 213, \"nameNl\": \"Weekdieren\", \"nameEn\": \"mollusks\", \"logo\": \"Weekdieren.gif\"},\n    {\"id\": 214, \"nameNl\": \"halal\", \"nameEn\": \"halal\", \"logo\": \"halal.gif\"},\n]\n\nCOURSE_LOGOS: Dict[str, int] = {\n    'BIO': 201,  # Biological course (???)\n    'CHICKEN': 202,  # Contains chicken\n    'GRILL': 203,  # Grill course\n    'CHEESE': 204,  # Contains cheese\n    'RABBIT': 205,  # Contains rabbit\n    'LAMB': 206,  # Contains lamb\n    'PASTA': 207,  # Pasta course / contains pasta???\n    'VEAL': 208,  # Contains veal\n    'SALAD': 209,  # Salad course\n    'SNACK': 210,  # Sub course\n    'SOUP': 211,  # Soup course\n    'PIG': 212,  # Contains pig\n    'VEGAN': 213,  # Vegan course\n    'VEGGIE': 214,  # Vegetarian course\n    'FISH': 215,  # Contains fish\n    'LESS_MEAT': 216,  # Contains less meat\n    'HEALTHIFY': 217,  # ???\n    'BROWN_BREAD': 218,  # Brown bread\n    'WHITE_BREAD': 219,  # White bread\n    'CONCEPT_BREAD': 220,  # ??? bread\n}\n\nCOURSE_LOGOS_REVERSE: Dict[int, str] = {value: key for key, value in COURSE_LOGOS.items()}\n\nCOURSE_ALLERGENS = {\n    'EGG': 200,\n    'WHEAT_GLUTEN': 201,\n    'LUPINE': 202,\n    'MILK_LACTOSE': 203,\n    'MUSTARD': 204,\n    'NUTS': 205,\n    'PEANUTS': 206,\n    'SHELLFISH': 207,\n    'CELERY': 208,\n    'SESAME': 209,\n    'SOY': 210,\n    'SULFITES': 211,\n    'FISH': 212,\n    'MOLLUSKS': 213,\n    'HALAL': 214,\n}\n\nCOURSE_ALLERGENS_REVERSE: Dict[int, str] = {value: key for key, value in COURSE_ALLERGENS.items()}\n\nPASTA_NAMES = ['spaghetti', 'tagliatelle', 'papardelle', 'bucatini', 'cannelloni',\n               'ravioli', 'tortellini', 'caramelle', 'penne', 'rigatoni', 'orecchiette',\n               'farfalle', 'caserecce', 'fusilli', 'pasta', ]\n# Pasta names for those who don't speak Italian\nBROKEN_ITALIAN_NAMES = ['spagheti', 'tagliatele', 'papardele', 'bucatinni',\n                        'cannellonni', 'canneloni', 'cannellonni', 'raviolli',\n                        'tortellinni', 'tortelini', 'tortelinni', 'caramele', 'pene',\n                        'rigatonni', 'orecchiete', 'orechiette', 'orechiete', 'farfale',\n                        'caserece', 'fusili', ]\n\nsession_obj = requests.Session()\nlimiter = Limiter(5)  # Limit to 5 lookups per second\n\n\ndef _cleanup_session(session: requests.Session):\n    session.close()\n\n\natexit.register(_cleanup_session, session_obj)\n\n\ndef _convert_price(price_students: Union[str, Decimal]) -> Decimal:\n    url = PRICE_API.format(endpoint=BASE_ENDPOINT, price=price_students)\n    price_response = session_obj.get(url, headers=API_GET_HEADERS)\n    price_data = json.loads(price_response.text)\n\n    return round(Decimal(price_data['staffprice']), 2)\n\n\ndef _decimal_or_none(value: str) -> Optional[Decimal]:\n    if value is None:\n        return None\n    return Decimal(value)\n\n\ndef fetch_raw(campus: models.Campus, date: datetime.date) -> Optional[Any]:\n    debug_state = ProgramStateTrace()\n\n    with debug_state.state(SimpleProgramState('Lookup menu', {'campus': campus.short_name, 'date': date.isoformat()})):\n        limiter()\n\n        url = MENU_API.format(endpoint=BASE_ENDPOINT, campus=campus.external_id, date=date.strftime('%Y-%m-%d'))\n\n        try:\n            response = session_obj.get(url, headers=API_GET_HEADERS)\n        except requests.exceptions.Timeout:\n            return None  # If the connection times out, we'll just ignore it\n\n        if 400 <= response.status_code < 500:\n            raise DebuggableException('Client error on HTTP request')\n        if 500 <= response.status_code < 600:\n            # raise DebuggableException('Server error on HTTP request')\n            return None  # Don't raise an exception when the server fails, we'll just ignore it\n            # TODO: Maybe send a notification to admins that we failed requesting data?\n\n        # No content is returned when there is no menu for a campus on a specific day\n        if response.status_code == 204:\n            return None\n\n        try:\n            return json.loads(response.text)\n        except json.decoder.JSONDecodeError:\n            # If we fail to decode JSON, this means we got an invalid response back\n            # This can (or used to) happen when we try to look up the menu on a Sunday or Saturday\n            return None\n\n\ndef parse_fetched(fetched: Dict):\n    if fetched is None:\n        return None\n\n    debug_state = ProgramStateTrace()\n\n    campus = models.Campus.get_by_external_id(fetched['restaurantId'])\n\n    result = {\n        'date': datetime.datetime.strptime(fetched['menuDate'], '%Y-%m-%dT%H:%M:%S').date().isoformat(),\n        'campus': campus.short_name,\n        'menu': []\n    }\n\n    for raw_item in fetched['menuItems']:\n        with debug_state.state(SimpleProgramState('Menu item', raw_item['id'])):\n            if raw_item['enabled'] != 1:  # XXX: Spotted in the wild, enabled values of 2!\n                continue\n\n            parsed_item = {\n                'external_id': raw_item['id'],\n                'components': [],\n                'price': Decimal(0),\n                'multiple_prices': False,\n                'sort_order': raw_item['sortorder']\n            }\n\n            # Sort components in place\n            # XXX: This makes the items order consistent in the output as well\n            raw_item['menuItemContents'].sort(key=lambda v: (not v['course']['showFirst'],\n                                                             not v['course']['maincourse'],\n                                                             v['sortOrder']))\n\n            for raw_item_contents in raw_item['menuItemContents']:\n                with debug_state.state(SimpleProgramState('Menu item component', raw_item_contents['id'])):\n                    raw_course = raw_item_contents['course']\n\n                    if not raw_course['enabled']:\n                        pass  # XXX: Used to skip not enabled, but the official site shows these items anyway (bug?)\n\n                    if raw_course['deleted']:\n                        pass  # XXX: Used to skip deleted, but the official site shows these items anyway (bug?)\n\n                    # XXX: Note on names, sometimes these can contain double spaces, so we normalize them.\n                    #      We also strip any whitespace from the start and end of the names\n                    component = {\n                        'name': {\n                            'nl': re.sub(r'\\s+', ' ', raw_course['dispNameNl']).strip(),\n                        },\n                        'attributes': [],\n                        'allergens': []\n                    }\n\n                    if raw_course['dispNameEn']:\n                        component['name']['en'] = re.sub(r'\\s+', ' ', raw_course['dispNameEn']).strip()\n\n                    parsed_item['price'] += round(Decimal(raw_course['price']), 2)\n\n                    if raw_course['calculatedMultiplePrices'] or raw_course['fixedMultiplePrices']:\n                        parsed_item['multiple_prices'] = True\n\n                    for raw_allergens in raw_course['course_Allergens']:\n                        component['allergens'].append(COURSE_ALLERGENS_REVERSE[raw_allergens['allergenId']])\n\n                    for raw_logos in raw_course['course_CourseLogos']:\n                        component['attributes'].append(COURSE_LOGOS_REVERSE[raw_logos['courseLogoId']])\n\n                    # Ensure consistent output\n                    component['allergens'].sort()\n                    component['attributes'].sort()\n\n                    parsed_item['components'].append(component)\n\n            if parsed_item['price'] == 0:\n                continue  # Items with no price are most likely informational messages, not courses\n\n            parsed_item['price'] = str(parsed_item['price'])\n\n            # XXX: Only add a menu item if there's actually something in it\n            if len(parsed_item['components']) > 0:\n                result['menu'].append(parsed_item)\n\n    # Ensure consistent output\n    result['menu'].sort(key=lambda v: v['external_id'])\n\n    return result\n\n\ndef process_parsed(parsed: Dict):\n    if parsed is None:\n        return None\n\n    debug_state = ProgramStateTrace()\n\n    result = {\n        'date': parsed['date'],\n        'campus': parsed['campus'],\n        'menu': [],\n    }\n\n    for parsed_item in parsed['menu']:\n        with debug_state.state(SimpleProgramState('Menu item', parsed_item['external_id'])):\n            processed_item = {\n                'external_id': parsed_item['external_id'],\n                'name': {\n                    'nl': [],\n                    'en': []\n                },\n                'course_type': '',\n                'course_sub_type': '',\n                'course_attributes': set(),\n                'course_allergens': set(),\n                'price_students': parsed_item['price'],\n                'price_staff': None\n            }\n\n            for component in parsed_item['components']:\n                component: Dict\n\n                with debug_state.state(SimpleProgramState('Menu item component', component)):\n                    processed_item['course_attributes'].update(component['attributes'])\n                    processed_item['course_allergens'].update(component['allergens'])\n\n                    if 'nl' in processed_item['name']:\n                        # If not in here, then a component did not support this language\n                        piece = component['name'].get('nl', '')\n                        if not piece:\n                            # Remove if not every component supports this language\n                            del processed_item['name']['nl']\n                        else:\n                            processed_item['name']['nl'].append(piece)\n\n                    if 'en' in processed_item['name']:\n                        # If not in here, then a component did not support this language\n                        piece = component['name'].get('en', '')\n                        if not piece:\n                            # Remove if not every component supports this language\n                            del processed_item['name']['en']\n                        else:\n                            processed_item['name']['en'].append(piece)\n\n            for lang in processed_item['name']:\n                name = ', '.join(processed_item['name'][lang])\n                name = name[0].upper() + name[1:]\n                processed_item['name'][lang] = name\n\n            processed_item['course_attributes'] = list(processed_item['course_attributes'])\n            processed_item['course_attributes'].sort()\n\n            processed_item['course_allergens'] = list(processed_item['course_allergens'])\n            processed_item['course_allergens'].sort()\n\n            if parsed_item['multiple_prices']:\n                processed_item['price_staff'] = str(_convert_price(parsed_item['price']))\n\n            has_pasta = 'PASTA' in processed_item['course_attributes']\n\n            if not has_pasta:\n                # No pasta in name, let's check to make sure anyway\n                name = processed_item['name']['nl']\n\n                for pasta in PASTA_NAMES + BROKEN_ITALIAN_NAMES:\n                    if pasta in name.lower():\n                        has_pasta = True\n                        break\n\n            course_type = models.CourseType.DAILY\n            course_sub_type = models.CourseSubType.NORMAL\n\n            if 'VEGAN' in processed_item['course_attributes']:\n                course_sub_type = models.CourseSubType.VEGAN\n            elif 'VEGGIE' in processed_item['course_attributes']:\n                course_sub_type = models.CourseSubType.VEGETARIAN\n\n            if 'SOUP' in processed_item['course_attributes']:\n                course_type = models.CourseType.SOUP\n            elif 'PASTA' in processed_item['course_attributes'] or has_pasta:\n                course_type = models.CourseType.PASTA\n            elif 'GRILL' in processed_item['course_attributes']:\n                course_type = models.CourseType.GRILL\n            elif 'SNACK' in processed_item['course_attributes']:\n                # If the item has a low price, it's more likely to be a snack, not a sub (broodje)\n                if Decimal(processed_item['price_students']) < 2.7:\n                    course_type = models.CourseType.SNACK\n                else:\n                    course_type = models.CourseType.SUB\n            elif 'SALAD' in processed_item['course_attributes']:\n                course_type = models.CourseType.SALAD\n            else:\n                # If the item has a low price and no other specific logo, it's probably a dessert, not a daily course\n                if Decimal(processed_item['price_students']) < 3:\n                    course_type = models.CourseType.DESSERT\n\n            processed_item['course_type'] = course_type.name\n            processed_item['course_sub_type'] = course_sub_type.name\n\n            result['menu'].append(processed_item)\n\n    return result\n\n\ndef update_menu(processed: Dict):\n    if processed is None:\n        return None\n\n    debug_state = ProgramStateTrace()\n\n    with debug_state.state(SimpleProgramState('Campus menu update', {'campus': processed['campus'],\n                                                                     'date': processed['date']})):\n        items = processed['menu']\n        if len(items) > 0:\n            campus = models.Campus.get_by_short_name(processed['campus'])\n            date = datetime.date.fromisoformat(processed['date'])\n\n            menu = models.Menu.get_menu(campus, date)\n\n            if menu is None:\n                menu = models.Menu.create(campus, date)\n\n            external_ids = [item['external_id'] for item in items]\n            menu_items = {}\n\n            for menu_item in menu.menu_items:\n                if menu_item.external_id not in external_ids:  # Also matches if menu_item.external_id is None\n                    if not menu_item.data_frozen:\n                        # Old item, remove\n                        db.session.delete(menu_item)\n                else:\n                    menu_items[menu_item.external_id] = menu_item\n\n            for item in items:\n                translatable, translation = models.Translatable.get_or_create(item['name'][LANGUAGE_DUTCH],\n                                                                              LANGUAGE_DUTCH)\n\n                for language in set(item['name'].keys()).difference([LANGUAGE_DUTCH]):\n                    if translatable.has_translation(language):\n                        translation = translatable.get_translation(language)\n\n                        # Don't replace translation if provider is Komida, as this is the official translation\n                        # Likewise, if the provider is not defined, this means it is most likely manually added\n                        # Otherwise it's done by Google or some other provider, which is sub-optimal\n                        if translation.provider not in [None, 'komida', 'manual']:\n                            continue  # XXX: Only continues for loop over languages\n\n                        # Update translation and provider to new values\n                        translation.translation = item['name'][language]\n                        translation.provider = 'komida'\n                    else:\n                        translatable.add_translation(language, item['name'][language], 'komida')\n\n                attributes = [models.CourseAttributes[attribute] for attribute in item['course_attributes']]\n                allergens = [models.CourseAllergens[allergen] for allergen in item['course_allergens']]\n\n                if item['external_id'] in menu_items:\n                    menu_item = menu_items[item['external_id']]\n                    if not menu_item.data_frozen:\n                        menu_item.translatable = translatable\n                        menu_item.course_type = models.CourseType[item['course_type']]\n                        menu_item.course_sub_type = models.CourseSubType[item['course_sub_type']]\n                        menu_item.set_attributes(attributes)\n                        menu_item.set_allergens(allergens)\n                        menu_item.price_students = Decimal(item['price_students'])\n                        menu_item.price_staff = _decimal_or_none(item['price_staff'])\n                else:\n                    menu_item = menu.add_menu_item(translatable,\n                                                   models.CourseType[item['course_type']],\n                                                   models.CourseSubType[item['course_sub_type']],\n                                                   attributes, allergens,\n                                                   Decimal(item['price_students']),\n                                                   _decimal_or_none(item['price_staff']))\n                    menu_item.external_id = item['external_id']\n"
  },
  {
    "path": "komidabot/facebook/api_interface.py",
    "content": "import json\nimport threading\n\nimport requests\nfrom cachetools import cachedmethod, TTLCache\n\nimport komidabot.messages as messages\nfrom komidabot.app import get_app\nfrom komidabot.translation import LANGUAGE_DUTCH\nfrom komidabot.util import check_exceptions\n\nBASE_ENDPOINT = 'https://graph.facebook.com/'\nAPI_VERSION = 'v4.0'\nSEND_API = '/me/messages'\nPROFILE_API = '/me/messenger_profile'\nPASS_THREAD_CONTROL_API = '/me/pass_thread_control'\n\n\nclass ApiInterface:\n    def __init__(self, page_access_token: str):\n        self.session = requests.Session()\n\n        self.base_parameters = dict()\n        self.base_parameters['access_token'] = page_access_token\n        self.headers_post = dict()\n        self.headers_post['Content-Type'] = 'application/json'\n\n        self.locale_parameters = dict()\n        self.locale_parameters['access_token'] = page_access_token\n        self.locale_parameters['fields'] = 'locale'\n\n        self.locale_cache = TTLCache(maxsize=64, ttl=300)\n        self.locale_lock = threading.Lock()\n\n    @check_exceptions(messages.MessageSendResult.ERROR)  # Handles exceptions raised in this method\n    def post_send_api(self, data: dict) -> messages.MessageSendResult:\n        response = self.session.post(BASE_ENDPOINT + API_VERSION + SEND_API, params=self.base_parameters,\n                                     headers=self.headers_post, data=json.dumps(data))\n        data = json.loads(response.content)\n\n        app = get_app()\n\n        if app.config.get('VERBOSE'):\n            print('Received {} for request {}'.format(response.status_code, response.request.body), flush=True)\n            print(response.content, flush=True)\n\n        if response.status_code == 200:\n            return messages.MessageSendResult.SUCCESS\n\n        if 500 <= response.status_code < 600:\n            return messages.MessageSendResult.EXTERNAL_ERROR\n\n        if response.status_code == 400:\n            code = data['error']['code']\n            subcode = data['error']['error_subcode']\n\n            # https://developers.facebook.com/docs/messenger-platform/reference/send-api/error-codes\n            if code == 1200:\n                # Temporary send message failure. Please try again later.\n                return messages.MessageSendResult.EXTERNAL_ERROR\n            if code == 100:\n                if subcode == 2018001:\n                    # No matching user found\n                    return messages.MessageSendResult.GONE\n            if code == 10:\n                if subcode == 2018065:\n                    # This message is sent outside of allowed window.\n                    return messages.MessageSendResult.UNREACHABLE\n                if subcode == 2018108:\n                    # This Person Cannot Receive Messages: This person isn't receiving messages from you right now.\n                    return messages.MessageSendResult.UNREACHABLE\n                if subcode == 2018278:\n                    # TODO: Get official description from FB once available\n                    # Sent after March 4th to indicate the subscription message was denied\n                    return messages.MessageSendResult.UNREACHABLE\n            if code == 551:\n                if subcode == 1545041:\n                    # This person isn't available right now.\n                    return messages.MessageSendResult.UNREACHABLE\n\n        return messages.MessageSendResult.ERROR  # TODO: Further specify\n\n    @check_exceptions(False)  # TODO: Exception checking needs to be done differently\n    def post_profile_api(self, data: dict):\n        response = self.session.post(BASE_ENDPOINT + API_VERSION + PROFILE_API, params=self.base_parameters,\n                                     headers=self.headers_post, data=json.dumps(data))\n\n        app = get_app()\n\n        if app.config.get('VERBOSE'):\n            print('Received {} for request {}'.format(response.status_code, response.request.body), flush=True)\n            print(response.content, flush=True)\n\n        # response.raise_for_status()\n\n        # return True\n\n        return response.status_code == 200\n\n    @check_exceptions(False)  # TODO: Exception checking needs to be done differently\n    def post_pass_thread_control(self, data: dict):\n        response = self.session.post(BASE_ENDPOINT + API_VERSION + PASS_THREAD_CONTROL_API, params=self.base_parameters,\n                                     headers=self.headers_post, data=json.dumps(data))\n\n        app = get_app()\n\n        if app.config.get('VERBOSE'):\n            print('Received {} for request {}'.format(response.status_code, response.request.body), flush=True)\n            print(response.content, flush=True)\n\n        # response.raise_for_status()\n\n        # return True\n\n        return response.status_code == 200\n\n    @check_exceptions()  # TODO: Exception checking needs to be done differently\n    @cachedmethod(lambda self: self.locale_cache, lock=lambda self: self.locale_lock)\n    def lookup_locale(self, user_id: str) -> str:\n        # TODO: Futures or Promises???\n\n        response = self.session.get(BASE_ENDPOINT + API_VERSION + user_id, params=self.locale_parameters)\n\n        # print('Received {} for user request {}'.format(response.status_code, user_id), flush=True)\n        # print(response.content, flush=True)\n\n        data = json.loads(response.content)\n\n        return data.get('locale', LANGUAGE_DUTCH)\n"
  },
  {
    "path": "komidabot/facebook/constants.py",
    "content": "PROVIDER_ID = 'facebook'\n"
  },
  {
    "path": "komidabot/facebook/messages.py",
    "content": "import komidabot.facebook.constants as fb_constants\nimport komidabot.menu\nimport komidabot.messages as messages\nimport komidabot.triggers as triggers\nimport komidabot.users as users\nfrom komidabot.app import get_app\n\nTYPE_REPLY = 'RESPONSE'\nTYPE_SUBSCRIPTION = 'NON_PROMOTIONAL_SUBSCRIPTION'\n\n\nclass MessageHandler(messages.MessageHandler):\n    def send_message(self, user: users.User, message: messages.Message) -> messages.MessageSendResult:\n        if user.id.provider != fb_constants.PROVIDER_ID:\n            raise ValueError('User id is not for Facebook')\n\n        if isinstance(message, messages.TextMessage):\n            return self._send_text_message(user.id, message)\n        elif isinstance(message, messages.MenuMessage):\n            return self._send_menu_message(user, message)\n        elif isinstance(message, TemplateMessage):\n            return self._send_template_message(user.id, message)\n        else:\n            return messages.MessageSendResult.UNSUPPORTED\n\n    @staticmethod\n    def _send_text_message(user_id: users.UserId, message: messages.TextMessage) -> messages.MessageSendResult:\n        data = {\n            'recipient': {\n                'id': user_id.id\n            },\n            'message': {\n                'text': message.text\n            },\n            'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION,\n        }\n\n        return get_app().bot_interfaces['facebook']['api_interface'].post_send_api(data)\n\n    @staticmethod\n    def _send_menu_message(user: users.User, message: messages.MenuMessage) -> messages.MessageSendResult:\n        text = komidabot.menu.get_menu_text(message.menu, message.translator, user.get_locale())\n\n        if text is None:\n            return messages.MessageSendResult.ERROR\n\n        data = {\n            'recipient': {\n                'id': user.get_internal_id()\n            },\n            'message': {\n                'text': text\n            },\n            'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION,\n        }\n\n        return get_app().bot_interfaces['facebook']['api_interface'].post_send_api(data)\n\n    @staticmethod\n    def _send_template_message(user_id: users.UserId, message: 'TemplateMessage') -> messages.MessageSendResult:\n        data = {\n            'recipient': {\n                'id': user_id.id\n            },\n            'message': {\n                'attachment': {\n                    'type': 'template',\n                    'payload': message.payload\n                }\n            },\n            'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION,\n        }\n\n        return get_app().bot_interfaces['facebook']['api_interface'].post_send_api(data)\n\n\nclass TemplateMessage(messages.Message):\n    def __init__(self, trigger: messages.Trigger, payload):\n        super().__init__(trigger)\n        self.payload = payload\n"
  },
  {
    "path": "komidabot/facebook/nlp_dates.py",
    "content": "from typing import List\n\nimport dateutil.parser as date_parser\n\nimport komidabot.triggers as triggers\n\n\ndef extract_days(aspects: List[triggers.DatetimeAspect]):\n    dates = []\n    invalid_date = False\n\n    for attribute in aspects:\n        grain = attribute.grain\n        value = attribute.value\n\n        if grain is None or value is None:\n            continue\n\n        # TODO: Date parsing could be a lot better\n        # Ex. vanmiddag is rejected\n\n        if grain == 'day':\n            date = date_parser.isoparse(value).date()\n            dates.append(date)\n        else:\n            invalid_date = True\n\n    return dates, invalid_date\n"
  },
  {
    "path": "komidabot/facebook/postbacks.py",
    "content": "import json\nfrom typing import Callable, Dict, Optional\n\nimport komidabot.facebook.messages as fb_messages\nimport komidabot.facebook.triggers as triggers\nimport komidabot.localisation as localisation\nimport komidabot.messages as messages\nimport komidabot.models as models\nfrom extensions import db\nfrom komidabot.translation import LANGUAGE_DUTCH, LANGUAGE_ENGLISH\n\npostback_mappings = {}\n\n\nclass Postback:\n    def call_postback(self, trigger: triggers.Trigger, *args, **kwargs) -> triggers.Trigger:\n        raise NotImplementedError()\n\n\ndef lookup_postback(name: str) -> Postback:\n    return postback_mappings.get(name, None)\n\n\ndef postback(name: str = None):\n    class PostbackDecorator(Postback):\n        def __init__(self, func: Callable):\n            nonlocal name\n\n            if name is None:\n                name = func.__name__\n\n            if name in postback_mappings:\n                raise ValueError('Duplicate postback identifier')\n\n            postback_mappings[name] = self\n\n            self.func = func\n            self.__name__ = func.__name__\n\n        def call_postback(self, trigger: triggers.Trigger, *args, **kwargs) -> Optional[triggers.Trigger]:\n            return self.func(trigger, *args, **kwargs)\n\n        def __call__(self, *args, **kwargs):\n            return json.dumps({'name': name, 'args': args, 'kwargs': kwargs})\n\n    return PostbackDecorator\n\n\ndef postback_button(title: str, payload: str):\n    return {'type': 'postback', 'title': title, 'payload': payload}\n\n\ndef url_button(title: str, url: str):\n    return {\n        'type': 'web_url',\n        'url': url,\n        'title': title,\n        'webview_height_ratio': 'full',\n        'messenger_extensions': 'false',\n    }\n\n\n@postback(name='komidabot:get_started')\ndef get_started(trigger: triggers.Trigger):\n    if triggers.NewUserAspect not in trigger:\n        trigger.add_aspect(triggers.NewUserAspect())\n    return trigger\n\n\n@postback(name='komidabot:menu_today')\ndef menu_today(trigger: triggers.Trigger):\n    return trigger\n\n\n@postback(name='komidabot:settings_subscriptions')\ndef settings_subscriptions(trigger: triggers.Trigger):\n    if triggers.SenderAspect not in trigger:\n        raise ValueError('Trigger missing SenderAspect')\n    sender = trigger[triggers.SenderAspect].sender\n    db_user = sender.get_db_user()\n    locale = sender.get_locale()\n\n    if not sender.is_feature_active('menu_subscription'):\n        sender.send_message(messages.TextMessage(trigger, localisation.REPLY_FEATURE_UNAVAILABLE(locale)))\n        return None\n\n    current_subscriptions = {item.day: (item.campus_id if item.active else None) for item in\n                             models.UserDayCampusPreference.get_all_for_user(db_user)}\n    current_subscriptions: Dict[models.Day, Optional[int]]\n\n    elements_list = [[]]\n\n    campuses = models.Campus.get_all_active()\n\n    for day in models.week_days:\n        elements = []\n        current = current_subscriptions.get(day, None)\n\n        title = localisation.DAYS[day.value - 1](locale).capitalize()\n        buttons = []\n        if current is None:\n            buttons.append(postback_button('✔️ ' + localisation.UNSUBSCRIBED(locale),\n                                           set_subscription(day.value, None)))\n        else:\n            buttons.append(postback_button(localisation.UNSUBSCRIBE(locale),\n                                           set_subscription(day.value, None)))\n\n        for campus in campuses:\n            if current == campus.id:\n                buttons.append(postback_button('✔️ ' + campus.name,\n                                               set_subscription(day.value, campus.id)))\n            else:\n                buttons.append(postback_button(campus.name,\n                                               set_subscription(day.value, campus.id)))\n\n        for i in range(0, len(buttons), 3):\n            elements.append({\n                'title': title if i == 0 else (title + localisation.CONTINUATION(locale)),\n                # 'image_url': image,\n                'buttons': buttons[i:i + 3]\n            })\n\n        if len(elements_list[-1]) + len(elements) > 10:\n            elements_list.append([])\n\n        elements_list[-1].extend(elements)\n\n    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_EXPERIMENTAL_DISPLAY(locale)))\n\n    for elements in elements_list:\n        payload = {\n            'template_type': 'generic',\n            'elements': elements,\n        }\n        sender.send_message(fb_messages.TemplateMessage(trigger, payload))\n\n    return None\n\n\n@postback(name='komidabot:set_subscription')\ndef set_subscription(trigger: triggers.Trigger, day: int, campus: Optional[int]):\n    if triggers.SenderAspect not in trigger:\n        raise ValueError('Trigger missing SenderAspect')\n    sender = trigger[triggers.SenderAspect].sender\n    db_user = sender.get_db_user()\n    locale = sender.get_locale()\n\n    if not sender.is_feature_active('menu_subscription'):\n        sender.send_message(messages.TextMessage(trigger, localisation.REPLY_FEATURE_UNAVAILABLE(locale)))\n        return None\n\n    selected_day = models.Day(day)\n    selected_campus = None\n\n    if campus is None:\n        db_user.set_day_active(selected_day, False)\n    else:\n        selected_campus = models.Campus.get_by_id(campus)\n        db_user.set_campus(selected_day, selected_campus, active=True)\n\n    db.session.commit()\n\n    msg = localisation.REPLY_SET_SUBSCRIPTION(locale).format(day=localisation.DAYS[day - 1](locale),\n                                                             campus=localisation.UNSUBSCRIBED(locale)\n                                                             if selected_campus is None else selected_campus.name)\n    sender.send_message(messages.TextMessage(trigger, msg))\n\n    return None\n\n\n@postback(name='komidabot:settings_language')\ndef settings_language(trigger: triggers.Trigger):\n    if triggers.SenderAspect not in trigger:\n        raise ValueError('Trigger missing SenderAspect')\n    sender = trigger[triggers.SenderAspect].sender\n\n    payload = {\n        'template_type': 'button',\n        'text': 'Chose your desired language',\n        'buttons': [\n            postback_button(\"Nederlands\", set_language(LANGUAGE_DUTCH, 'Nederlands')),\n            postback_button(\"English\", set_language(LANGUAGE_ENGLISH, 'English')),\n            postback_button(\"From Facebook\", set_language('', 'From Facebook')),\n        ],\n    }\n    sender.send_message(fb_messages.TemplateMessage(trigger, payload))\n\n    return None\n\n\n@postback(name='komidabot:set_language')\ndef set_language(trigger: triggers.Trigger, language: str, display: str):\n    if triggers.SenderAspect not in trigger:\n        raise ValueError('Trigger missing SenderAspect')\n    sender = trigger[triggers.SenderAspect].sender\n    db_user = sender.get_db_user()\n    locale = sender.get_locale()\n\n    db_user.set_language(language)\n    db.session.commit()\n\n    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_SET_LANGUAGE(locale).format(language=display)))\n\n    return None\n\n\ndef generate_postback_data(include_persistent_menu: bool, production: bool):\n    result = dict()\n    result['get_started'] = {\n        'payload': get_started(),\n    }\n    result['greeting'] = [\n        {\n            'locale': 'default',\n            'text': 'Welcome!',\n        },\n        {\n            'locale': 'nl_BE',\n            'text': 'Welkom!',\n        },\n        {\n            'locale': 'nl_NL',\n            'text': 'Welkom!',\n        },\n    ]\n    if include_persistent_menu:\n        menu = [\n            postback_button(\"Today's menu\", menu_today()),\n            postback_button(\"Change language\", settings_language())\n        ]\n\n        if not production:\n            menu.append(url_button(\"Open Komidabot.xyz\", 'https://dev.komidabot.xyz/'))\n\n        # TODO: Once per-user persistent menus are available, use them\n        #       https://developers.facebook.com/docs/messenger-platform/send-messages/persistent-menu/\n        #       Followup: What for?\n        result['persistent_menu'] = [\n            {\n                'locale': 'default',\n                'composer_input_disabled': False,\n                'call_to_actions': menu,\n            },\n        ]\n\n    return result\n"
  },
  {
    "path": "komidabot/facebook/triggers.py",
    "content": "from komidabot.triggers import *\n\n\nclass PostbackTrigger(Trigger):\n    def __init__(self, name, d_args, d_kwargs, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.name = name\n        self.args = d_args\n        self.kwargs = d_kwargs\n\n    def get_repr_text(self):\n        return ['PostbackTrigger',\n                '- Name: ' + repr(self.name),\n                '- args: ' + repr(self.args),\n                '- kwargs: ' + repr(self.kwargs),\n                ]\n"
  },
  {
    "path": "komidabot/facebook/users.py",
    "content": "from typing import Optional, Union\n\nimport komidabot.facebook.constants as fb_constants\nimport komidabot.messages as messages\nimport komidabot.models as models\nimport komidabot.users as users\nfrom komidabot.app import get_app\nfrom komidabot.facebook.messages import MessageHandler as FBMessageHandler\n\n__all__ = ['User', 'UserManager']\n\n\nclass UserManager(users.UserManager):\n    def __init__(self):\n        self.message_handler = FBMessageHandler()\n\n    # def get_subscribed_users(self, day: models.Day) -> 'List[users.User]':\n    #     # TODO: Starting March 4th 2020, facebook subscriptions will no longer be available\n    #     #       https://developers.facebook.com/docs/messenger-platform/policy/policy-overview/\n    #     # return super().get_subscribed_users(day)\n    #     return []\n\n    def get_user(self, user: 'Union[users.UserId, models.AppUser]', **kwargs) -> 'User':\n        if isinstance(user, models.AppUser):\n            return User(self, user.internal_id)\n\n        if user.provider != fb_constants.PROVIDER_ID:\n            raise ValueError('User id is not for {}'.format(fb_constants.PROVIDER_ID))\n\n        # TODO: This probably could use more checks or something\n        #       For example: check if there is a subscription\n        return User(self, user.id)\n\n    def initialise(self):\n        import komidabot.facebook.postbacks as postbacks\n\n        app = get_app()\n        if app.config.get('TESTING') or app.config.get('DISABLED'):\n            return\n\n        data = postbacks.generate_postback_data(True, app.config.get('PRODUCTION'))\n        app.bot_interfaces['facebook']['api_interface'].post_profile_api(data)\n\n    def get_identifier(self):\n        return fb_constants.PROVIDER_ID\n\n\nclass User(users.User):\n    def __init__(self, manager: UserManager, id_str: str):\n        self._manager = manager\n        self._id = id_str\n\n    def get_locale(self) -> 'Optional[str]':\n        stored_value = super().get_locale()\n\n        if not stored_value:\n            return get_app().bot_interfaces['facebook']['api_interface'].lookup_locale(self._id)\n\n        return stored_value\n\n    def get_provider_name(self) -> 'str':\n        return fb_constants.PROVIDER_ID\n\n    def get_internal_id(self) -> 'str':\n        return self._id\n\n    def supports_subscription_channel(self, channel: str) -> bool:\n        # Facebook users cannot receive subscriptions anymore\n        # This used to work by sending a message with \"messaging_type\" set to \"NON_PROMOTIONAL_SUBSCRIPTION\"\n        # See https://developers.facebook.com/docs/messenger-platform/policy/policy-overview/\n        return False\n\n    def get_manager(self) -> UserManager:\n        return self._manager\n\n    def get_message_handler(self) -> messages.MessageHandler:\n        return self._manager.message_handler\n\n    def mark_message_seen(self):\n        return get_app().bot_interfaces['facebook']['api_interface'].post_send_api({\n            'recipient': {'id': self._id},\n            'sender_action': 'mark_seen'\n        })\n"
  },
  {
    "path": "komidabot/features.py",
    "content": "from collections import namedtuple\nfrom typing import Dict, Optional\n\nimport komidabot.models as models\nfrom extensions import db\nfrom komidabot.users import UserId\n\n_feature = namedtuple('_feature', ['string_id', 'description', 'globally_available', 'active_users'])\n_features = [\n    _feature('menu_subscription', 'The user can receive a daily menu message automatically', True, [\n        # Dev user ID\n        UserId('3150885824953769', 'facebook'),\n        # Production user IDs\n        UserId('1441134665935530', 'facebook'),\n        UserId('1532346296833228', 'facebook'),\n    ]),\n    _feature('new_site_notifications', 'The user can receive a notification about the new site', True, [\n        # Dev user ID\n        UserId('3150885824953769', 'facebook'),\n        # Production user IDs\n        UserId('1441134665935530', 'facebook'),\n    ]),\n]\n\n\nclass _Feature:\n    def __init__(self, feat: 'Optional[_feature]', obj: 'Optional[models.Feature]'):\n        self.feat = feat\n        self.obj = obj\n\n    def __repr__(self):\n        return '_Feature({}, {})'.format(repr(self.feat), repr(self.obj))\n\n\ndef update_active_features():\n    print('Updating active features', flush=True)\n\n    current_features = models.Feature.get_all()\n\n    feature_mapping = dict()  # type: Dict[str, _Feature]\n    for feature in current_features:\n        feature_mapping[feature.string_id] = _Feature(None, feature)\n\n    for feature in _features:\n        if feature.string_id not in feature_mapping:\n            feature_mapping[feature.string_id] = _Feature(feature, None)\n        else:\n            feature_mapping[feature.string_id].feat = feature\n\n    # print('Features mapping: {}'.format(feature_mapping), flush=True)\n\n    removed_features = [feature.obj for feature in feature_mapping.values() if feature.feat is None]\n\n    for feature in removed_features:  # type: models.Feature\n        print('Removing feature {}: {}'.format(feature.string_id, feature.description or 'no description'), flush=True)\n        db.session.delete(feature)\n\n    db.session.commit()\n\n    new_features = [feature.feat for feature in feature_mapping.values() if feature.obj is None]\n\n    for feature in new_features:  # type: _feature\n        print('Adding new feature {}: {}'.format(feature.string_id, feature.description or 'no description'),\n              flush=True)\n        models.Feature.create(feature.string_id, feature.description, feature.globally_available)\n\n        for user_id in feature.active_users:  # type: UserId\n            user = models.AppUser.find_by_id(user_id.provider, user_id.id)\n            if user is None:\n                print('Skipping user {} for feature {}'.format(user_id, feature.string_id),\n                      flush=True)\n                continue\n            print('Adding user {} to new feature {}'.format(user_id, feature.string_id),\n                  flush=True)\n            models.Feature.set_user_participating(user, feature.string_id, True)\n\n    db.session.commit()\n\n    existing_features = [feature for feature in feature_mapping.values()\n                         if feature.feat is not None and feature.obj is not None]\n\n    for feature in existing_features:  # type: _Feature\n        if feature.feat.globally_available != feature.obj.globally_available:\n            print('Updating existing feature {}: {}'.format(feature.obj.string_id,\n                                                            feature.obj.description or 'no description'), flush=True)\n            print('Changing general availability to {}'.format(feature.feat.globally_available), flush=True)\n\n            feature.obj.globally_available = feature.feat.globally_available\n        if feature.feat.description != feature.obj.description:\n            print('Updating existing feature {}: {}'.format(feature.obj.string_id,\n                                                            feature.obj.description or 'no description'), flush=True)\n            print('Changing description to {}'.format(feature.feat.description), flush=True)\n\n            feature.obj.description = feature.feat.description\n\n    db.session.commit()\n\n    print('Done updating active features', flush=True)\n"
  },
  {
    "path": "komidabot/komidabot.py",
    "content": "import atexit\nimport datetime\nimport threading\nfrom typing import List\n\nfrom apscheduler.executors.pool import ThreadPoolExecutor\nfrom apscheduler.jobstores.memory import MemoryJobStore\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\n\nimport komidabot.external_menu as external_menu\nimport komidabot.facebook.nlp_dates as nlp_dates\nimport komidabot.localisation as localisation\nimport komidabot.messages as messages\nimport komidabot.triggers as triggers\nfrom extensions import db\nfrom komidabot.app import get_app\nfrom komidabot.bot import Bot\nfrom komidabot.debug.state import DebuggableException, ProgramStateTrace, SimpleProgramState\nfrom komidabot.models import Campus, ClosingDays, Day, Menu\nfrom komidabot.models import create_standard_values, import_dump, recreate_db\n\n\nclass Komidabot(Bot):\n    def __init__(self, the_app):\n        self.lock = threading.Lock()\n\n        self.scheduler = BackgroundScheduler(\n            jobstores={'default': MemoryJobStore()},\n            executors={'default': ThreadPoolExecutor(max_workers=4)},\n            job_defaults={'misfire_grace_time': 60}\n        )\n\n        self._handling_error = False\n\n        # Scheduled jobs should work with DST\n\n        @self.scheduler.scheduled_job(CronTrigger(day_of_week='mon-fri', hour=10, minute=0, second=0),\n                                      args=(the_app.app_context, self),\n                                      id='daily_menu', name='Daily menu notifications')\n        def daily_menu(context, bot: 'Komidabot'):\n            with context():\n                if get_app().config.get('DISABLED'):\n                    return\n\n                bot.trigger_received(triggers.SubscriptionTrigger())\n\n        @self.scheduler.scheduled_job(CronTrigger(minute=0, second=0),  # Run every hour to find changes\n                                      args=(the_app.app_context, self),\n                                      id='menu_update', name='Hourly update of the menus')\n        def menu_update(context, bot: 'Komidabot'):\n            with context():\n                if get_app().config.get('DISABLED'):\n                    return\n\n                try:\n                    today = datetime.datetime.today().date()\n                    week_start = today + datetime.timedelta(days=-today.weekday())\n\n                    dates = [week_start + datetime.timedelta(days=i) for i in range(today.weekday(), 5)]\n                    if today.weekday() >= 3:\n                        dates += [week_start + datetime.timedelta(days=7 + i) for i in range(5)]\n\n                    update_menus(dates=dates)\n                except DebuggableException as e:\n                    bot.notify_error(e)\n\n                    e.print_info(get_app().logger)\n                except Exception as e:\n                    bot.notify_error(e)\n\n                    get_app().logger.exception(e)\n\n    def start_scheduler(self):\n        self.scheduler.start()\n        atexit.register(BackgroundScheduler.shutdown, self.scheduler)  # Ensure cleanup of resources\n\n    def trigger_received(self, trigger: triggers.Trigger):\n        with self.lock:  # TODO: Maybe only lock on critical sections?\n            app = get_app()\n            verbose = app.config.get('VERBOSE')\n\n            if verbose:\n                print('Komidabot received a trigger: {}'.format(type(trigger).__name__), flush=True)\n                print(repr(trigger), flush=True)\n\n            if isinstance(trigger, triggers.SubscriptionTrigger):\n                dispatch_daily_menus(trigger)\n                return\n\n            if triggers.AtAdminAspect in trigger:\n                return  # Don't process messages targeted at the admin\n\n            locale = None\n            message_handled = False\n\n            # XXX: Disabled once more because responses aren't reliably in the language the user expects it to be\n            # if triggers.LocaleAspect in trigger and trigger[triggers.LocaleAspect].confidence > 0.9:\n            #     locale = trigger[triggers.LocaleAspect].locale\n\n            if triggers.SenderAspect in trigger:\n                sender = trigger[triggers.SenderAspect].sender\n                campuses = Campus.get_all()\n\n                # This ensures that when a user is marked as reachable in case they were unreachable at some point\n                # TODO: We no longer mark users as reachable, need to think over the proper course of action\n                # if sender.mark_reachable():\n                #     db.session.commit()\n\n                if locale is None:\n                    locale = sender.get_locale()\n\n                if triggers.NewUserAspect in trigger:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_NEW_USER(locale)))\n                    msg = localisation.REPLY_INSTRUCTIONS(locale).format(\n                        campuses=', '.join([campus.short_name.lower() for campus in campuses if campus.active])\n                    )\n                    sender.send_message(messages.TextMessage(trigger, msg))\n                    sender.set_is_notified_new_site(True)\n                    db.session.commit()\n\n                    message_handled = True\n\n                # TODO: Is this really how we want to handle input?\n                #       Maybe we can add an IntentAspect, where the intent is the desired action the bot should take\n                #       next? Ex. intents: admin message, get help, get menu, set preference (language, subscriptions)\n                if isinstance(trigger, triggers.TextTrigger):\n                    text = trigger.text\n                    split = text.lower().split(' ')\n\n                    if sender.is_admin():\n                        if split[0] == 'setup':\n                            if app.config.get('PRODUCTION'):\n                                sender.send_message(messages.TextMessage(trigger, 'Not running setup on production'))\n                                return\n                            recreate_db()\n                            create_standard_values()\n                            import_dump(app.config['DUMP_FILE'])\n                            sender.send_message(messages.TextMessage(trigger, 'Setup done'))\n                            return\n                        elif split[0] == 'update':\n                            sender.send_message(messages.TextMessage(trigger, 'Updating menus...'))\n                            update_menus(*split[1:])\n                            sender.send_message(messages.TextMessage(trigger, 'Done updating menus...'))\n                            return\n                        elif split[0] == 'psid':  # TODO: Deprecated?\n                            sender.send_message(messages.TextMessage(trigger, 'Your ID is {}'.format(sender.id.id)))\n                            return\n\n                    # TODO: Allow users to send more manual commands\n                    #       See also the note prefacing the containing block\n                    if not message_handled and split[0] == 'help':\n                        msg = localisation.REPLY_INSTRUCTIONS(locale).format(\n                            campuses=', '.join([campus.short_name.lower() for campus in campuses if campus.active])\n                        )\n                        sender.send_message(messages.TextMessage(trigger, msg))\n                        return\n\n                if app.config.get('COVID19_DISABLED'):\n                    sender.send_message(messages.TextMessage(trigger, localisation.COVID19_UNAVAILABLE(locale)))\n                    return\n\n                requested_dates = []\n                default_date = False\n\n                if triggers.DatetimeAspect in trigger:\n                    date_times = trigger[triggers.DatetimeAspect]\n                    # TODO: Date parsing needs improving\n                    requested_dates, invalid_date = nlp_dates.extract_days(date_times)\n\n                    if invalid_date:\n                        sender.send_message(messages.TextMessage(trigger, localisation.REPLY_INVALID_DATE(locale)))\n                        return\n\n                if len(requested_dates) > 1:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_TOO_MANY_DAYS(locale)))\n                    return\n                elif len(requested_dates) == 1:\n                    date = requested_dates[0]\n                else:\n                    default_date = True\n                    date = datetime.datetime.now().date()\n\n                # TODO: How about getting the menu for the next day after a certain time of day?\n                #       Only if we're returning the default day\n\n                day = Day(date.isoweekday())\n\n                if day == Day.SATURDAY or day == Day.SUNDAY:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_WEEKEND(locale)))\n                    return\n\n                requested_campuses = []\n                default_campus = False\n\n                if isinstance(trigger, triggers.TextTrigger):\n                    text = trigger.text.lower()\n                    for campus in campuses:\n                        if not campus.active:\n                            continue\n\n                        for kw in campus.get_keywords():\n                            if text.count(kw) > 0:\n                                requested_campuses.append(campus)\n                                break  # Prevent the same campus from being added multiple times\n\n                if len(requested_campuses) > 1:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_TOO_MANY_CAMPUSES(locale)))\n                    return\n                elif len(requested_campuses) == 1:\n                    campus = requested_campuses[0]\n                else:\n                    default_campus = True\n                    campus = sender.get_campus_for_day(date)\n\n                    if campus is None:  # User has no campus for the specified day\n                        campus = Campus.get_by_short_name('cmi')\n\n                if not campus.active:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_CAMPUS_INACTIVE(locale)\n                                                             .format(campus=campus.name)))\n                    return\n\n                if message_handled and default_campus and default_date:\n                    if isinstance(trigger, triggers.TextTrigger):\n                        for word in ['menu', 'lunch', 'eten']:\n                            if word in trigger.text:\n                                break\n                        else:\n                            return\n                    else:\n                        return\n\n                # if default_date and default_campus:\n                #     if isinstance(trigger, triggers.TextTrigger):\n                #         sender.send_message(messages.TextMessage(trigger,\n                # localisation.REPLY_NO_DATE_OR_CAMPUS(locale)))\n                #         msg = localisation.REPLY_INSTRUCTIONS(locale).format(\n                #             campuses=', '.join([campus.short_name for campus in campuses])\n                #         )\n                #         sender.send_message(messages.TextMessage(trigger, msg))\n                #         return\n                #\n                #     # User did not send a text message, so we'll continue anyway\n\n                if not default_campus:\n                    sender.set_campus_for_day(campus, date)\n                    db.session.commit()\n\n                if sender.get_is_notified_new_site() is False and sender.is_feature_active('new_site_notifications'):\n                    if sender.send_message(messages.TextMessage(trigger, localisation.MESSAGE_NEW_SITE(locale))) \\\n                            == messages.MessageSendResult.SUCCESS:\n                        sender.set_is_notified_new_site(True)\n                        db.session.commit()\n\n                closed = ClosingDays.find_is_closed(campus, date)\n\n                if closed:\n                    translation = closed.translatable.get_translation(locale, app.translator)\n\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_CAMPUS_CLOSED(locale)\n                                                             .format(campus=campus.name, date=str(date),\n                                                                     reason=translation.translation)))\n                    return\n\n                # menu = komidabot.menu.prepare_menu_text(campus, date, app.translator, locale)\n                menu = Menu.get_menu(campus, date)\n\n                if menu is None:\n                    sender.send_message(messages.TextMessage(trigger, localisation.REPLY_NO_MENU(locale)\n                                                             .format(campus=campus.name, date=str(date))))\n                else:\n                    # sender.send_message(messages.TextMessage(trigger, menu))\n                    sender.send_message(messages.MenuMessage(trigger, menu, app.translator))\n\n                # XXX: Disabled experiment\n                # if default_date and default_campus and isinstance(trigger, triggers.TextTrigger):\n                #     for keyword in ['lunch', 'menu', 'komida']:\n                #         if keyword.lower() in trigger.text.lower():\n                #             break\n                #     else:\n                #         sender.send_message(messages.TextMessage(trigger, localisation.REPLY_USE_AT_ADMIN(locale)))\n\n    def notify_error(self, error: Exception):\n        if self._handling_error:\n            # Already handling an error, or we failed handling the previous error, so don't try handling more\n            return\n        self._handling_error = True\n\n        self.message_admins(messages.ExceptionMessage(triggers.Trigger(), error))\n\n        self._handling_error = False\n\n    def message_admins(self, message: messages.Message):\n        from komidabot.debug.administration import notify_admins\n\n        with self.lock:\n            notify_admins(message)\n\n\ndef dispatch_daily_menus(trigger: triggers.SubscriptionTrigger):\n    from komidabot.subscriptions.daily_menu import CHANNEL_ID as DAILY_MENU_ID\n\n    # limiter = Limiter(20)  # Limit to 20 messages per second\n\n    date = trigger.date or datetime.datetime.now().date()\n    day = Day(date.isoweekday())\n\n    app = get_app()\n\n    verbose = app.config.get('VERBOSE')\n\n    if verbose:\n        print('Sending out subscription for {} ({})'.format(date, day.name), flush=True)\n\n    message = messages.SubscriptionMenuMessage(trigger, date, app.translator)\n    app.subscription_manager.deliver_message(DAILY_MENU_ID, message)\n\n    # user_manager = app.user_manager\n    # changed = False\n    #\n    # subscribed_users = user_manager.get_subscribed_users(day)\n    # subscriptions: Dict[Campus, List[users.User]] = dict()\n    #\n    # for user in subscribed_users:\n    #     if app.config.get('DISABLED') and not user.is_admin():\n    #         continue\n    #\n    #     if not user.is_feature_active('menu_subscription'):\n    #         if verbose:\n    #             print('User {} not eligible for subscription'.format(user.id), flush=True)\n    #         continue\n    #\n    #     subscription = user.get_subscription_for_day(date)\n    #     if subscription is None:\n    #         continue\n    #     if not subscription.active:\n    #         continue\n    #\n    #     campus = subscription.campus\n    #\n    #     if not campus.active:\n    #         continue\n    #\n    #     if campus not in subscriptions:\n    #         subscriptions[campus] = []\n    #\n    #     subscriptions[campus].append(user)\n    #\n    # for campus, sub_users in subscriptions.items():\n    #     if verbose:\n    #         print('Preparing menu for {}'.format(campus.short_name), flush=True)\n    #\n    #     closed = ClosingDays.find_is_closed(campus, date)\n    #\n    #     if closed:\n    #         continue  # Campus closed, no daily menu\n    #\n    #     # TODO: Change menus from TextMessage to a custom message type to support different formatting per platform\n    #     menu = Menu.get_menu(campus, date)\n    #     if menu is None:\n    #         continue\n    #\n    #     for user in sub_users:\n    #         limiter()  # Ensure we don't send too many messages at once\n    #\n    #         if verbose:\n    #             print('Sending menu for {} to {}'.format(campus.short_name, user.id), flush=True)\n    #         message_result = user.send_message(messages.MenuMessage(trigger, menu, app.translator))\n    #\n    #         if message_result == messages.MessageSendResult.UNSUPPORTED:\n    #             # Text messages unsupported? Disable subscription then\n    #             print('User {} does not support messages, removing from subscription list'.format(user.id),\n    #                   flush=True)\n    #\n    #             user.mark_unreachable()\n    #             changed = True\n    #         if message_result == messages.MessageSendResult.UNREACHABLE:\n    #             # Unreachable = Facebook is blocking us from sending, stop trying to send in the future\n    #             print('User {} is unreachable, removing from subscription list'.format(user.id), flush=True)\n    #\n    #             user.mark_unreachable()\n    #             changed = True\n    #         if message_result == messages.MessageSendResult.GONE:\n    #             # Gone = User no longer exists, delete from database\n    #             print('User {} is gone, removing from database'.format(user.id), flush=True)\n    #\n    #             user.delete()\n    #             changed = True\n    #\n    # if changed:\n    #     db.session.commit()\n\n\ndef update_menus(*campuses: str, dates: 'List[datetime.date]' = None):\n    debug_state = ProgramStateTrace()\n\n    campus_list = Campus.get_all_active()\n\n    if len(campuses) > 0:\n        campus_list = [campus for campus in campus_list if campus.short_name not in campuses]\n\n    if not dates:\n        today = datetime.datetime.today().date()\n        dates = [\n            today,\n            today + datetime.timedelta(days=1),\n            today + datetime.timedelta(days=2),\n            today + datetime.timedelta(days=3),\n            today + datetime.timedelta(days=4),\n            today + datetime.timedelta(days=5),\n            today + datetime.timedelta(days=6),\n            today + datetime.timedelta(days=7),\n        ]\n\n    for campus in campus_list:\n        for date in dates:\n            if date.isoweekday() in [6, 7]:\n                continue\n\n            closed = ClosingDays.find_is_closed(campus, date)\n\n            if closed:\n                continue  # Campus closed, don't try to find a menu\n\n            with debug_state.state(SimpleProgramState('Campus menu update', {'campus': campus.short_name,\n                                                                             'date': str(date)})):\n                data_raw = external_menu.fetch_raw(campus, date)\n                data_parsed = external_menu.parse_fetched(data_raw)\n                data_processed = external_menu.process_parsed(data_parsed)\n\n                if data_processed is None:\n                    continue  # No data\n\n                assert campus.short_name == data_processed['campus']\n                assert date.isoformat() == data_processed['date']\n\n                external_menu.update_menu(data_processed)\n\n    db.session.commit()\n"
  },
  {
    "path": "komidabot/localisation.py",
    "content": "import random\n\nfrom typing import Callable\n\n\ndef localisation_definition(name, obj, fallback='en') -> Callable[[str], str]:\n    for key, value in obj.copy().items():\n        if isinstance(key, tuple):\n            del obj[key]\n            for k in key:\n                obj[k] = value\n\n    def wrapper(locale):\n        if locale is None:\n            result = obj[fallback]\n        else:\n            locale = locale.lower().split('_', 1)[0]\n            result = obj[locale] if locale in obj else obj[fallback]\n\n        if callable(result):\n            return result()\n        elif isinstance(result, list):\n            weights, strings = zip(*result)\n            return random.choices(strings, weights=weights)\n        else:\n            return result\n\n    wrapper.__name__ = name\n\n    return wrapper\n\n\n# Supported locales:\n#   https://developers.facebook.com/docs/messenger-platform/messenger-profile/supported-locales\n\nINTERNAL_ERROR = localisation_definition('INTERNAL_ERROR', {\n    'en': 'An unexpected error occured while trying to perform your request',\n    'nl': [\n        (1, 'oepsie woepsie! de bot is stukkie wukkie! we sijn heul hard '\n            'aan t werk om dit te make mss kan je beter self kijken  owo'),\n        (99, 'Een onverwachte fout gebeurde tijdens het uitvoeren van uw verzoek'),\n    ],\n})\n\n# INTERNAL_ERROR = localisation_definition('INTERNAL_ERROR', {\n#     'en': 'An unexpected error occured while trying to perform your request',\n#     'nl': 'Een onverwachte fout gebeurde tijdens het uitvoeren van uw verzoek',\n# })\n\nERROR_TEXT_ONLY = localisation_definition('ERROR_TEXT_ONLY', {\n    'en': 'Sorry, I only understand text messages',\n    'nl': 'Sorry, ik begrijp alleen tekstberichten',\n})\n\nERROR_NOT_IMPLEMENTED = localisation_definition('ERROR_NOT_IMPLEMENTED', {\n    'en': 'Sorry, this feature is currently not implemented',\n    'nl': 'Sorry, deze feature is momenteel niet geïmplementeerd',\n})\n\nERROR_POSTBACK = localisation_definition('ERROR_POSTBACK', {\n    'en': 'Sorry, I cannot handle that message right now. '\n          'Please try sending a message using the textbox instead.',\n    'nl': 'Sorry, ik kan dit bericht momenteel niet begrijpen. '\n          'Gelieve het tekstvak te gebruiken voor uw vraag.',\n})\n\nREPLY_NO_MENU = localisation_definition('REPLY_NO_MENU', {\n    'en': 'Sorry, no menu is available for {campus} on {date}',\n    'nl': 'Sorry, er is geen menu beschikbaar voor {campus} op {date}',\n})\n\nREPLY_CAMPUS_CLOSED = localisation_definition('REPLY_NO_MENU', {\n    'en': 'Sorry, no menu is available for {campus} on {date}: {reason}',\n    'nl': 'Sorry, er is geen menu beschikbaar voor {campus} op {date}: {reason}',\n})\n\nREPLY_CAMPUS_INACTIVE = localisation_definition('REPLY_CAMPUS_INACTIVE', {\n    'en': 'Sorry, no menus are available for {campus}',\n    'nl': 'Sorry, er zijn geen menus beschikbaar voor {campus}',\n})\n\nREPLY_WEEKEND = localisation_definition('REPLY_WEEKEND', {\n    'en': 'Sorry, there are no menus on Saturdays and Sundays',\n    'nl': 'Sorry, er zijn geen menus op zon- en zaterdagen',\n})\n\nREPLY_TOO_MANY_DAYS = localisation_definition('REPLY_TOO_MANY_DAYS', {\n    'en': 'Sorry, please request only a single day',\n    'nl': 'Sorry, gelieve een enkele dag te specificeren',\n})\n\nREPLY_INVALID_DATE = localisation_definition('REPLY_INVALID_DATE', {\n    'en': 'Sorry, I am unable to understand the requested day. '\n          'Please try to specify the day as e.g. \"Monday\" or \"Tomorrow\"',\n    'nl': 'Sorry, ik kan de gevraagde dag niet begrijpen. '\n          'Gelieve de dag aan te geven als bvb. \"Maandag\" of \"Morgen\"',\n})\n\nREPLY_TOO_MANY_CAMPUSES = localisation_definition('REPLY_TOO_MANY_CAMPUSES', {\n    'en': 'Sorry, please only ask for a single campus at a time',\n    'nl': 'Sorry, gelieve een enkele campus te specificeren',\n})\n\nREPLY_MENU_START = localisation_definition('REPLY_MENU_START', {\n    'en': 'Menu at {campus} on {date}',\n    'nl': 'Menu van {date} in {campus}',\n})\n\nREPLY_MENU_INCOMPLETE = localisation_definition('REPLY_MENU_START', {\n    'en': '⚠️ NOTE: This menu may be incomplete',\n    'nl': '⚠️ LET OP: Dit menu is mogelijks incompleet',\n})\n\nREPLY_USE_AT_ADMIN = localisation_definition('REPLY_USE_AT_ADMIN', {\n    'en': \"If you would like to talk to the admin instead, use @admin in your message and \"\n          \"I won't disturb you\\n~ 🤖 Komidabot\",\n    'nl': 'Als je met de admin wilt praten, dan kan je @admin gebruiken en '\n          'zal ik je niet storen\\n~ 🤖 Komidabot',\n})\n\nREPLY_NEW_USER = localisation_definition('REPLY_NEW_USER', {\n    'en': 'Welcome to the Komidabot!',\n    'nl': 'Welkom bij de Komidabot!',\n})\n\nREPLY_INSTRUCTIONS = localisation_definition('REPLY_INSTRUCTIONS', {\n    'en': 'You can request the menu by choosing a campus ({campuses}) and/or '\n          'asking for a specific day (Monday - Friday, Today, Tomorrow, etc.)\\n\\n'\n          'To reach the admin, you can use @admin.\\n\\n'\n          'You can also check out the menu at https://komidabot.xyz/',\n    'nl': 'Je kan het menu opvragen door een campus te kiezen ({campuses}) en/of '\n          'een specifieke dag te vragen (maandag - vrijdag, vandaag, morgen, etc.)\\n\\n'\n          'Om de admin te bereiken, kan je @admin gebruiken.\\n\\n'\n          'Verder kan je het menu ook raadplegen op https://komidabot.xyz/',\n})\n\nDOWN_FOR_MAINTENANCE = localisation_definition('DOWN_FOR_MAINTENANCE', {\n    'en': 'I am temporarily down for maintenance, please check back later',\n    'nl': 'Wegens onderhoud ben ik tijdelijk onbeschikbaar, probeer het later nog eens',\n})\n\nDAYS = [\n    localisation_definition('DAYS[0]', {\n        'en': 'Monday',\n        'nl': 'maandag',\n    }),\n    localisation_definition('DAYS[1]', {\n        'en': 'Tuesday',\n        'nl': 'dinsdag',\n    }),\n    localisation_definition('DAYS[2]', {\n        'en': 'Wednesday',\n        'nl': 'woensdag',\n    }),\n    localisation_definition('DAYS[3]', {\n        'en': 'Thursday',\n        'nl': 'donderdag',\n    }),\n    localisation_definition('DAYS[4]', {\n        'en': 'Friday',\n        'nl': 'vrijdag',\n    }),\n    localisation_definition('DAYS[5]', {\n        'en': 'Saturday',\n        'nl': 'zaterdag',\n    }),\n    localisation_definition('DAYS[6]', {\n        'en': 'Sunday',\n        'nl': 'zondag',\n    }),\n]\n\nMONTHS = [\n    localisation_definition('MONTHS[0]', {'en': 'January', 'nl': 'januari', }),\n    localisation_definition('MONTHS[1]', {'en': 'February', 'nl': 'februari', }),\n    localisation_definition('MONTHS[2]', {'en': 'March', 'nl': 'maart', }),\n    localisation_definition('MONTHS[3]', {'en': 'April', 'nl': 'april', }),\n    localisation_definition('MONTHS[4]', {'en': 'May', 'nl': 'mei', }),\n    localisation_definition('MONTHS[5]', {'en': 'June', 'nl': 'Juni', }),\n    localisation_definition('MONTHS[6]', {'en': 'July', 'nl': 'juli', }),\n    localisation_definition('MONTHS[7]', {'en': 'August', 'nl': 'augustus', }),\n    localisation_definition('MONTHS[8]', {'en': 'September', 'nl': 'september', }),\n    localisation_definition('MONTHS[9]', {'en': 'October', 'nl': 'october', }),\n    localisation_definition('MONTHS[10]', {'en': 'November', 'nl': 'november', }),\n    localisation_definition('MONTHS[11]', {'en': 'December', 'nl': 'december', }),\n]\n\nCONTINUATION = localisation_definition('CONTINUATION', {\n    'en': ' (cont.)',\n    'nl': ' (vervolg)',\n})\n\nSELECTED = localisation_definition('SELECTED', {\n    'en': ' (current)',\n    'nl': ' (geselecteerd)',\n})\n\nUNSUBSCRIBE = localisation_definition('UNSUBSCRIBE', {\n    'en': 'Unsubscribe',\n    'nl': 'Uitschrijven',\n})\n\nUNSUBSCRIBED = localisation_definition('UNSUBSCRIBED', {\n    'en': 'Unsubscribed',\n    'nl': 'Uitgeschreven',\n})\n\nREPLY_EXPERIMENTAL_DISPLAY = localisation_definition('REPLY_EXPERIMENTAL_DISPLAY', {\n    'en': 'This feature display is experimental and will change in the future.',\n    'nl': 'De weergave van deze feature is experimenteel en zal veranderen in de toekomst.',\n})\n\nREPLY_FEATURE_UNAVAILABLE = localisation_definition('REPLY_FEATURE_UNAVAILABLE', {\n    'en': 'This feature is currently unavailable.',\n    'nl': 'Deze feature is momenteel niet beschikbaar.',\n})\n\nREPLY_SET_SUBSCRIPTION = localisation_definition('REPLY_SET_SUBSCRIPTION', {\n    'en': 'Preference for {day} set to: {campus}',\n    'nl': 'Voorkeur voor {day} gezet op: {campus}',\n})\n\nREPLY_SET_LANGUAGE = localisation_definition('REPLY_SET_SUBSCRIPTION', {\n    'en': 'Your language is now set to: {language}',\n    'nl': 'Uw taal staat nu op: {language}',\n})\n\nMESSAGE_NEW_SITE = localisation_definition('MESSAGE_NEW_SITE', {\n    'en': \"Dear user, a new simplified way of viewing the Komida menus is now available by browsing to \"\n          \"https://komidabot.xyz/\\n\\n\"\n          \"To get an extended overview of the menus, including allergens and ingredients, you can always check the \"\n          \"official Komida website at https://restickets.uantwerpen.be/calendar\\n\\n\"\n          \"Of course the bot will always remain available to get the daily menu as well ;)\",\n    'nl': \"Beste gebruiker, vanaf nu kan u de menu's van de Komida op een simpele manier bekijken door naar \"\n          \"https://komidabot.xyz/ te surfen.\\n\\n\"\n          \"Voor een uitgebreider menu met informatie, inclusief allergenen en ingrediënten, kan u altijd de officiële \"\n          \"website van de Komida raadplegen op https://restickets.uantwerpen.be/kalender\\n\\n\"\n          \"Uiteraard blijft de bot hier altijd beschikbaar om de menu's op te vragen ;)\",\n})\n\nCOVID19_UNAVAILABLE = localisation_definition('COVID19_UNAVAILABLE', {\n    'en': 'Dear user, Komidabot is temporarily unable to display the menus for the Komida restaurants.\\n'\n          'For now, you can check out the menus and order online by following this link:'\n          'https://www.uantwerpen.be/en/life-in-antwerp/catering/about-komida/online-ordering/',\n    'nl': \"Beste gebruiker, de Komidabot kan tijdelijk geen menu's tonen voor de Komida.\\n\"\n          \"U kunt de menu's bekijken en bestellen door op deze link te klikken: \"\n          \"https://uantwerpen.be/nl/studentenleven/eten/over-komida/online-bestellen/\",\n})\n\n# MESSAGE_NO_SUBSCRIPTIONS = localisation_definition('REPLY_SET_SUBSCRIPTION', {\n#     'en': 'Dear user, from now on you can once again request the bot to send a daily menu at 10am.\\n\\n'\n#           'You can set this up by clicking on the \"Manage subscription\" button in the menu.\\n\\n'\n#           'Your preferences for this are per-day and can be changed at any moment.',\n#     'nl': 'Beste gebruiker, vanaf nu kan je de bot terug vragen om dagelijks het menu naar je te sturen.\\n\\n'\n#           'Je kan dit instellen door in het menu op \"Manage subscription\" te drukken.\\n\\n'\n#           'Uw voorkeuren hiervoor zijn per dag en kunnen op ieder moment aangepast worden.',\n# })\n#\n# MESSAGE_FIRST_SUBSCRIPTION = localisation_definition('REPLY_SET_SUBSCRIPTION', {\n#     'en': 'Dear user, from now on the bot will send you the daily menu at 10am once again.\\n\\n'\n#           'You can change your preferences by clicking on the \"Manage subscription\" button in the menu.\\n\\n'\n#           'Your preferences are per-day and can be changed at any moment.',\n#     'nl': 'Beste gebruiker, vanaf nu zal de bot terug automatisch het menu doorsturen om 10 uur.\\n\\n'\n#           'Je kan je voorkeuren aanpassen door in het menu op \"Manage subscription\" te drukken.\\n\\n'\n#           'Uw voorkeuren zijn per dag en kunnen op ieder moment aangepast worden.',\n# })\n"
  },
  {
    "path": "komidabot/menu.py",
    "content": "import datetime\nfrom typing import Optional\n\nimport komidabot.localisation as localisation\nimport komidabot.models as models\nimport komidabot.translation as translation\nimport komidabot.util as util\n\n\ndef get_menu_line(menu_item: models.MenuItem, translator: translation.TranslationService, locale: str = None) -> str:\n    translation_obj = menu_item.get_translation(locale, translator)\n\n    if not menu_item.price_staff:\n        price_str = models.MenuItem.format_price(menu_item.price_students)\n    else:\n        price_str = '{} / {}'.format(models.MenuItem.format_price(menu_item.price_students),\n                                     models.MenuItem.format_price(menu_item.price_staff))\n\n    return '{} {} ({})'.format(models.course_icons_matrix[menu_item.course_type][menu_item.course_sub_type],\n                               translation_obj.translation, price_str)\n\n\ndef prepare_menu_text(campus: models.Campus, date: datetime.date, translator: translation.TranslationService,\n                      locale: str) -> 'Optional[str]':\n    return get_menu_text(models.Menu.get_menu(campus, date), translator, locale)\n\n\ndef get_menu_text(menu: Optional[models.Menu], translator: translation.TranslationService,\n                  locale: str) -> 'Optional[str]':\n    if menu is None:\n        return None\n\n    date_str = util.date_to_string(locale, menu.menu_day)\n\n    result = [localisation.REPLY_MENU_START(locale).format(campus=menu.campus.name, date=date_str), '']\n\n    # if len(menu.menu_items) < 6:\n    #     result.insert(1, localisation.REPLY_MENU_INCOMPLETE(locale))\n\n    try:\n        for item in menu.menu_items:\n            item: models.MenuItem\n            result.append(get_menu_line(item, translator, locale))\n    except Exception:\n        print('Failed translating to {}'.format(locale), flush=True)\n        raise\n\n    return '\\n'.join(result)\n\n\ndef get_short_menu_text(menu: Optional[models.Menu], translator: translation.TranslationService,\n                        locale: str, *course_types: models.CourseType) -> 'Optional[str]':\n    if menu is None:\n        return None\n\n    result = []\n\n    try:\n        for item in menu.menu_items:\n            item: models.MenuItem\n            if course_types and item.course_type in course_types:\n                result.append(get_menu_line(item, translator, locale))\n    except Exception:\n        print('Failed translating to {}'.format(locale), flush=True)\n        raise\n\n    return '\\n'.join(result)\n"
  },
  {
    "path": "komidabot/messages.py",
    "content": "import datetime\nimport enum\nfrom typing import Any, Dict, List, Optional, Type, TypeVar, Union\n\nimport komidabot.models as models\nimport komidabot.translation as translation\n\n\nclass Aspect:\n    allows_multiple = False\n\n    def __repr__(self):\n        return 'Aspect'\n\n\nT = TypeVar('T')\n\n\nclass Trigger:\n    def __init__(self, aspects: List[Aspect] = None):\n        self._aspects: Dict[Type[Aspect], Union[List[Aspect], Aspect]] = dict()\n        if aspects:\n            for aspect in aspects:\n                self.add_aspect(aspect)\n\n    def add_aspect(self, aspect: Aspect, aspect_type: Type[Aspect] = None):\n        aspect_type = aspect_type or type(aspect)\n        if aspect_type in self._aspects:\n            if aspect_type.allows_multiple:\n                self._aspects[aspect_type].append(aspect)\n            else:\n                raise ValueError('Cannot add multiple aspects for ' + aspect_type.__name__)\n        else:\n            if aspect_type.allows_multiple:\n                self._aspects[aspect_type] = [aspect]\n            else:\n                self._aspects[aspect_type] = aspect\n\n    def __contains__(self, aspect_type: Type[Aspect]) -> bool:\n        return aspect_type in self._aspects\n\n    def __getitem__(self, aspect_type: Type[T]) -> Union[List[T], T]:\n        return self._aspects[aspect_type]\n\n    def __delitem__(self, aspect_type: Type[Aspect]):\n        del self._aspects[aspect_type]\n\n    @classmethod\n    def extend(cls: Type[T], trigger: 'Trigger', *args, aspects: List[Aspect] = None, **kwargs) -> T:\n        new_instance = cls(*args, **kwargs)\n        for aspect_type in trigger._aspects:\n            if not aspect_type.allows_multiple:\n                new_instance.add_aspect(trigger._aspects[aspect_type])\n            else:\n                for aspect in trigger._aspects[aspect_type]:\n                    new_instance.add_aspect(aspect)\n\n        if aspects:\n            for aspect in aspects:\n                new_instance.add_aspect(aspect)\n\n        return new_instance\n\n    def __repr__(self):\n        result = self.get_repr_text()\n        for aspect_type in self._aspects:\n            result.append('- ' + repr(self._aspects[aspect_type]))\n\n        return '\\n'.join(result)\n\n    def get_repr_text(self):\n        return ['Trigger']\n\n\nclass Message:\n    def __init__(self, trigger: Trigger):\n        self.trigger = trigger\n\n\nclass TextMessage(Message):\n    def __init__(self, trigger: Trigger, text: str):\n        super().__init__(trigger)\n        self.text = text\n\n\nclass ExceptionMessage(Message):\n    def __init__(self, trigger: Trigger, source: Exception):\n        super().__init__(trigger)\n        self.source = source\n\n\nclass MenuMessage(Message):\n    def __init__(self, trigger: Trigger, menu: models.Menu, translator: translation.TranslationService):\n        super().__init__(trigger)\n        self.menu = menu\n        self.translator = translator\n\n\nclass SubscriptionMenuMessage(Message):\n    def __init__(self, trigger: Trigger, date: datetime.date, translator: translation.TranslationService):\n        super().__init__(trigger)\n        self.date = date\n        self.translator = translator\n        # campus id -> {language -> {user manager -> prepared message}}\n        self.prepared_cache: Dict[int, Dict[str, Dict[str, Any]]] = dict()\n\n    def get_prepared(self, campus: models.Campus, lang: str, user_manager: str) -> Optional[Any]:\n        if campus.id in self.prepared_cache:\n            for_campus = self.prepared_cache[campus.id]\n            if lang in for_campus:\n                for_lang = for_campus[lang]\n                if user_manager in for_lang:\n                    return for_lang[user_manager]\n        return None\n\n    def set_prepared(self, campus: models.Campus, lang: str, user_manager: str, prepared: Any):\n        if campus.id not in self.prepared_cache:\n            self.prepared_cache[campus.id] = {}\n\n        for_campus = self.prepared_cache[campus.id]\n        if lang not in for_campus:\n            for_campus[lang] = {}\n\n        for_campus[lang][user_manager] = prepared\n\n\nclass MessageSendResult(enum.Enum):\n    # Indicates successful message sending\n    SUCCESS = 'Success'\n    # Indicates an internal error when sending\n    ERROR = 'Error'\n    # Indicates an external error when sending\n    EXTERNAL_ERROR = 'External error'\n    # Indicates the message could not be sent because the user does not support receiving it\n    UNSUPPORTED = 'Unsupported'\n    # Indicates the user could not be reached, but could potentially be reached in the future\n    UNREACHABLE = 'Unreachable'\n    # Indicates the user no longer exists, the user should be removed from the database\n    GONE = 'Gone'\n\n\nclass MessageHandler:\n    # NOTE: There are some cases where the result of this method is important\n    #       For example: When sending subscription messages, we cannot be certain the message will arrive, or the user\n    #       may have unsubscribed and we need to remove their entry from the database.\n    #       For cases where the message is a direct result of the user sending a message to us, we assume the message\n    #       will be delivered without problems.\n    def send_message(self, user, message: 'Message') -> 'MessageSendResult':\n        raise NotImplementedError()\n"
  },
  {
    "path": "komidabot/models.py",
    "content": "import datetime\nimport enum\nimport json\nimport locale\nfrom decimal import Decimal\nfrom typing import Any, Collection, Dict, List, Optional, Tuple\n\nfrom sqlalchemy import inspect as sqlalchemy_inspect\nfrom sqlalchemy.orm.session import make_transient, make_transient_to_detached\nfrom sqlalchemy.sql import expression\n\nfrom extensions import db, ModelBase\nfrom komidabot.translation import TranslationService\nfrom komidabot.util import expected, expected_or_none\n\nmake_transient = make_transient\nmake_transient_to_detached = make_transient_to_detached\n\n_KEYWORDS_SEPARATOR = ' '\n\n\n# Main course type\nclass CourseType(enum.Enum):\n    SOUP = 1\n    DAILY = 2\n    PASTA = 3\n    GRILL = 4\n    SALAD = 5\n    SUB = 6\n    DESSERT = 7\n    SNACK = 8\n\n\n# Course sub-type\nclass CourseSubType(enum.Enum):\n    NORMAL = 1\n    VEGETARIAN = 2\n    VEGAN = 3\n\n\n# Course attributes from external menu\nclass CourseAttributes(enum.Enum):\n    BIO = 201\n    CHICKEN = 202\n    GRILL = 203\n    CHEESE = 204\n    RABBIT = 205\n    LAMB = 206\n    PASTA = 207\n    VEAL = 208\n    SALAD = 209\n    SNACK = 210\n    SOUP = 211\n    PIG = 212\n    VEGAN = 213\n    VEGGIE = 214\n    FISH = 215\n    LESS_MEAT = 216\n    HEALTHIFY = 217\n    BROWN_BREAD = 218\n    WHITE_BREAD = 219\n    CONCEPT_BREAD = 220\n\n    @classmethod\n    def has_value(cls, value):\n        return value in cls._value2member_map_\n\n\n# Course attributes from external menu\nclass CourseAllergens(enum.Enum):\n    EGG = 200\n    WHEAT_GLUTEN = 201\n    LUPINE = 202\n    MILK_LACTOSE = 203\n    MUSTARD = 204\n    NUTS = 205\n    PEANUTS = 206\n    SHELLFISH = 207\n    CELERY = 208\n    SESAME = 209\n    SOY = 210\n    SULFITES = 211\n    FISH = 212\n    MOLLUSKS = 213\n    HALAL = 214\n\n    @classmethod\n    def has_value(cls, value):\n        return value in cls._value2member_map_\n\n\ncourse_icons_matrix = {\n    CourseType.SOUP: {\n        CourseSubType.NORMAL: '🍵',\n        CourseSubType.VEGETARIAN: '🍵',\n        CourseSubType.VEGAN: '🍵',\n    },\n    CourseType.DAILY: {\n        CourseSubType.NORMAL: '🥩',\n        CourseSubType.VEGETARIAN: '🥬',\n        CourseSubType.VEGAN: '🥬',\n    },\n    CourseType.PASTA: {\n        CourseSubType.NORMAL: '🍝',\n        CourseSubType.VEGETARIAN: '🍝',\n        CourseSubType.VEGAN: '🍝',\n    },\n    CourseType.GRILL: {\n        CourseSubType.NORMAL: '🍖',\n        CourseSubType.VEGETARIAN: '🍖',\n        CourseSubType.VEGAN: '🍖',\n    },\n    CourseType.SALAD: {\n        CourseSubType.NORMAL: '🥗',\n        CourseSubType.VEGETARIAN: '🥗',\n        CourseSubType.VEGAN: '🥗',\n    },\n    CourseType.SUB: {\n        CourseSubType.NORMAL: '🥖',\n        CourseSubType.VEGETARIAN: '🥖',\n        CourseSubType.VEGAN: '🥖',\n    },\n    CourseType.DESSERT: {\n        CourseSubType.NORMAL: '🍨',\n        CourseSubType.VEGETARIAN: '🍨',\n        CourseSubType.VEGAN: '🍨',\n    },\n    CourseType.SNACK: {\n        CourseSubType.NORMAL: '🥐',\n        CourseSubType.VEGETARIAN: '🥐',\n        CourseSubType.VEGAN: '🥐',\n    },\n}\n\n\nclass Day(enum.Enum):\n    MONDAY = 1\n    TUESDAY = 2\n    WEDNESDAY = 3\n    THURSDAY = 4\n    FRIDAY = 5\n    # Added for compat with datetime.date\n    SATURDAY = 6\n    SUNDAY = 7\n\n\nweek_days = [Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY]\n\n\nclass AppSettings(ModelBase):\n    __tablename__ = 'app_settings'\n\n    name = db.Column(db.String(), primary_key=True)\n    value = db.Column(db.String(), nullable=False, server_default=json.dumps(None))\n\n    def __init__(self, name: str, value: Any = None):\n        if not isinstance(name, str):\n            raise ValueError('name expected {} got {}'.format(type(str), type(name)))\n\n        self.name = name\n        self.value = json.dumps(value)\n\n    @staticmethod\n    def create_entries():\n        AppSettings.set_default('registrations_enabled', False)\n\n        db.session.commit()\n\n    @staticmethod\n    def set_default(name: str, default: Any) -> 'AppSettings':\n        setting = AppSettings.query.filter_by(name=name).first()\n\n        if setting is None:\n            setting = AppSettings(name, default)\n\n            db.session.add(setting)\n\n        return setting\n\n    @staticmethod\n    def get_value(name: str) -> Any:\n        setting = AppSettings.query.filter_by(name=name).first()\n\n        assert setting is not None\n\n        return json.loads(setting.value)\n\n\nclass Campus(ModelBase):\n    __tablename__ = 'campus'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    name = db.Column(db.String(128), nullable=False)\n    short_name = db.Column(db.String(8), nullable=False)\n    # TODO: Wouldn't it be easier to instead have a new table mapping keywords to campuses, resolving possible conflicts\n    keywords = db.Column(db.Text(), default='', nullable=False)\n    active = db.Column(db.Boolean(), default=True, nullable=False)\n    external_id = db.Column(db.Integer(), nullable=False)\n\n    menus = db.relationship('Menu', backref='campus', passive_deletes=True)\n    closing_days = db.relationship('ClosingDays', backref='campus', passive_deletes=True)\n    subscriptions = db.relationship('UserDayCampusPreference', backref='campus', passive_deletes=True)\n\n    def __init__(self, name: str, short_name: str):\n        if not isinstance(name, str):\n            raise expected('name', name, str)\n        if not isinstance(short_name, str):\n            raise expected('short_name', short_name, str)\n\n        self.name = name\n        self.short_name = short_name.lower()\n        self._set_keywords([short_name, ])\n\n    def get_keywords(self) -> List[str]:\n        return self.keywords.split(_KEYWORDS_SEPARATOR)\n\n    def add_keyword(self, keyword: str):\n        if _KEYWORDS_SEPARATOR in keyword:\n            raise ValueError('Cannot have a space (the separator) in a keyword: {}'.format(repr(keyword)))\n\n        self._set_keywords(self.get_keywords() + [keyword.lower(), ])\n\n    def remove_keyword(self, keyword: str):\n        self._set_keywords([kw for kw in self.get_keywords() if kw != keyword])\n\n    def _set_keywords(self, keywords: List[str]):\n        separator = _KEYWORDS_SEPARATOR\n        # XXX: Add separator at the front and end for queries\n        self.keywords = separator + separator.join(set(kw for kw in keywords if kw)) + separator\n\n    @staticmethod\n    def create(name: str, short_name: str, keywords: List[str], external_id: int, add_to_db=True) -> 'Campus':\n        result = Campus(name, short_name)\n        result.external_id = external_id\n\n        for keyword in keywords:\n            result.add_keyword(keyword)\n\n        if add_to_db:\n            db.session.add(result)\n\n        return result\n\n    @staticmethod\n    def get_by_id(campus_id: int) -> 'Optional[Campus]':\n        return Campus.query.filter_by(id=campus_id).first()\n\n    @staticmethod\n    def get_by_external_id(external_id: int) -> 'Optional[Campus]':\n        return Campus.query.filter_by(external_id=external_id).first()\n\n    @staticmethod\n    def get_by_short_name(short_name: str) -> 'Optional[Campus]':\n        return Campus.query.filter_by(short_name=short_name).first()\n\n    @staticmethod\n    def find_by_keyword(keyword: str) -> 'List[Campus]':\n        # XXX: Each keyword is prepended and appended with the separator\n        return Campus.query.filter(Campus.keywords.contains(_KEYWORDS_SEPARATOR + keyword.lower() + _KEYWORDS_SEPARATOR,\n                                                            autoescape=True)).all()\n\n    @staticmethod\n    def get_all() -> 'List[Campus]':\n        return Campus.query.order_by(Campus.id).all()\n\n    @staticmethod\n    def get_all_active() -> 'List[Campus]':\n        return Campus.query.filter_by(active=True).order_by(Campus.id).all()\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass ClosingDays(ModelBase):\n    __tablename__ = 'closing_days'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    campus_id = db.Column(db.Integer(), db.ForeignKey('campus.id'), nullable=False)\n    first_day = db.Column(db.Date(), nullable=False)\n    last_day = db.Column(db.Date(), nullable=True, server_default=None)\n    translatable_id = db.Column(db.Integer(), db.ForeignKey('translatable.id', onupdate='CASCADE', ondelete='RESTRICT'),\n                                nullable=False)\n\n    def __init__(self, campus_id: int, first_day: datetime.date, last_day: datetime.date,\n                 translatable_id: int):\n        if not isinstance(campus_id, int):\n            raise expected('campus_id', campus_id, int)\n        if not isinstance(first_day, datetime.date):\n            raise expected('first_day', first_day, datetime.date)\n        if last_day is not None and not isinstance(last_day, datetime.date):\n            raise expected_or_none('last_day', last_day, datetime.date)\n        if not isinstance(translatable_id, int):\n            raise expected('translatable_id', translatable_id, int)\n\n        self.campus_id = campus_id\n        self.first_day = first_day\n        self.last_day = last_day\n        self.translatable_id = translatable_id\n\n    @staticmethod\n    def create(campus: Campus, first_day: datetime.date, last_day: Optional[datetime.date], reason: str, language: str,\n               add_to_db=True) -> 'ClosingDays':\n        translatable, translation = Translatable.get_or_create(reason, language)\n\n        result = ClosingDays(campus.id, first_day, last_day, translatable.id)\n\n        if add_to_db:\n            db.session.add(result)\n\n        return result\n\n    @staticmethod\n    def find_is_closed(campus: Campus, day: datetime.date) -> 'Optional[ClosingDays]':\n        return ClosingDays.query.filter(db.and_(ClosingDays.campus_id == campus.id,\n                                                ClosingDays.first_day <= day,\n                                                db.or_(\n                                                    ClosingDays.last_day == None,\n                                                    ClosingDays.last_day >= day\n                                                )\n                                                )).first()\n\n    @staticmethod\n    def find_closing_days_including(campus: Campus,\n                                    start_date: datetime.date,\n                                    end_date: datetime.date) -> 'List[ClosingDays]':\n        return ClosingDays.query.filter(db.and_(ClosingDays.campus_id == campus.id,\n                                                ClosingDays.first_day <= end_date,\n                                                db.or_(\n                                                    ClosingDays.last_day == None,\n                                                    ClosingDays.last_day >= start_date\n                                                )\n                                                )).all()\n\n\nclass Translatable(ModelBase):\n    __tablename__ = 'translatable'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    original_language = db.Column(db.String(5), nullable=False)\n    original_text = db.Column(db.String(256), nullable=False)\n\n    _translations = db.relationship('Translation', backref='translatable', passive_deletes=True)\n    menu_items = db.relationship('MenuItem', backref='translatable')\n    closing_days = db.relationship('ClosingDays', backref='translatable')\n\n    def __init__(self, text: str, language: str):\n        if not isinstance(text, str):\n            raise expected('text', text, str)\n        if not isinstance(language, str):\n            raise expected('language', language, str)\n\n        self.original_language = language\n        self.original_text = text\n\n    def add_translation(self, language: str, text: str, provider: str = None) -> 'Translation':\n        if sqlalchemy_inspect(self).transient:\n            raise ValueError('Translatable is transient and cannot have translations')\n\n        if language == self.original_language:\n            return self._get_dummy_translation()\n\n        translation = Translation.query.filter_by(translatable_id=self.id, language=language).first()\n\n        if translation is None:\n            translation = Translation(self.id, language, text, provider)\n            db.session.add(translation)\n\n        return translation\n\n    def get_translation(self, language: str, translator: 'TranslationService' = None) -> 'Translation':\n        if not language:\n            raise ValueError('language expected (got {})'.format(language))\n        if translator is not None and not isinstance(translator, TranslationService):\n            raise expected_or_none('translator', translator, TranslationService)\n\n        if sqlalchemy_inspect(self).transient:\n            raise ValueError('Translatable is transient and cannot have translations')\n\n        if language == self.original_language:\n            return self._get_dummy_translation()\n\n        translation = Translation.query.filter_by(translatable_id=self.id, language=language).first()\n\n        if translation is None:\n            if translator is None:\n                raise ValueError('Cannot translate without translator function')\n\n            translation_text = translator.translate(self.original_text, self.original_language, language)\n\n            translation = self.add_translation(language, translation_text, translator.identifier)\n\n        return translation\n\n    def has_translation(self, language: str) -> 'bool':\n        if not language:\n            raise ValueError('language')\n\n        if sqlalchemy_inspect(self).transient:\n            raise ValueError('Translatable is transient and cannot have translations')\n\n        if language == self.original_language:\n            return True\n\n        return db.session.query(Translation.query.filter_by(translatable_id=self.id,\n                                                            language=language).exists()).scalar()\n\n    @property\n    def translations(self) -> 'Collection[Translation]':\n        return self._get_dummy_translation(), *list(self._translations)\n\n    def _get_dummy_translation(self) -> 'Translation':\n        translation = getattr(self, '_dummy_translation', None)\n        if translation is None:\n            # Make a fake Translation object\n            translation = Translation(self.id, self.original_language, self.original_text)\n            make_transient_to_detached(translation)\n\n        setattr(self, '_dummy_translation', translation)\n        return translation\n\n    @staticmethod\n    def get_or_create(text: str, language) -> 'Tuple[Translatable, Translation]':\n        translatable = Translatable.query.filter_by(original_language=language, original_text=text).first()\n\n        if translatable is None:\n            translatable = Translatable(text, language)\n            db.session.add(translatable)\n            db.session.flush()\n\n        return translatable, translatable.get_translation(language, None)\n\n    @staticmethod\n    def get_by_id(translatable_id) -> 'Optional[Translatable]':\n        return Translatable.query.filter_by(id=translatable_id).first()\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass Translation(ModelBase):\n    __tablename__ = 'translation'\n\n    translatable_id = db.Column(db.Integer(), db.ForeignKey('translatable.id', onupdate='CASCADE', ondelete='CASCADE'),\n                                primary_key=True)\n    language = db.Column(db.String(5), primary_key=True)\n    translation = db.Column(db.String(256), nullable=False)\n    provider = db.Column(db.String(16))\n\n    def __init__(self, translatable_id: int, language: str, translation: str, provider: str = None):\n        if not isinstance(translatable_id, int):\n            raise expected('translatable_id', translatable_id, int)\n        if not isinstance(language, str):\n            raise expected('language', language, str)\n        if not isinstance(translation, str):\n            raise expected('translation', translation, str)\n        if provider is not None and not isinstance(provider, str):\n            raise expected_or_none('provider', provider, str)\n\n        self.translatable_id = translatable_id\n        self.language = language\n        self.translation = translation\n        self.provider = provider\n\n    def __eq__(self, other: 'Translation'):\n        if self.translatable_id != other.translatable_id:\n            return False\n        if self.language != other.language:\n            return False\n        if self.translation != other.translation:\n            return False\n        return True\n\n    def __hash__(self):\n        return hash((self.translatable_id, self.language))\n\n\nclass Menu(ModelBase):\n    __tablename__ = 'menu'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    campus_id = db.Column(db.Integer(), db.ForeignKey('campus.id'), nullable=False)\n    menu_day = db.Column(db.Date(), nullable=False)\n\n    menu_items: 'Collection[MenuItem]' = db.relationship('MenuItem', backref='menu', passive_deletes=True,\n                                                         order_by='[MenuItem.course_type, MenuItem.course_sub_type]')\n\n    def __init__(self, campus_id: int, day: datetime.date):\n        if not isinstance(campus_id, int):\n            raise expected('campus_id', campus_id, int)\n        if not isinstance(day, datetime.date):\n            raise expected('day', day, datetime.date)\n\n        self.campus_id = campus_id\n        self.menu_day = day\n\n    def delete(self):\n        db.session.delete(self)\n\n    def add_menu_item(self, translatable: Translatable, course_type: CourseType, course_sub_type: CourseSubType,\n                      course_attributes: List[CourseAttributes], course_allergens: List[CourseAllergens],\n                      price_students: Decimal, price_staff: Optional[Decimal]) -> 'MenuItem':\n        menu_item = MenuItem(self, translatable.id, course_type, course_sub_type, price_students, price_staff)\n        menu_item.set_attributes(course_attributes)\n        menu_item.set_allergens(course_allergens)\n\n        # FIXME: Is this safe?\n        self.menu_items.append(menu_item)\n\n        return menu_item\n\n    @staticmethod\n    def create(campus: Campus, day: datetime.date, add_to_db=True) -> 'Menu':\n        menu = Menu(campus.id, day)\n\n        if add_to_db:\n            db.session.add(menu)\n\n        return menu\n\n    @staticmethod\n    def get_menu(campus: Campus, day: datetime.date) -> 'Optional[Menu]':\n        return Menu.query.filter_by(campus_id=campus.id, menu_day=day).first()\n\n    @staticmethod\n    def remove_menus_on_closing_days():\n        rows = Menu.query.filter(\n            ClosingDays.query.filter(\n                Menu.campus_id == ClosingDays.campus_id,\n                Menu.menu_day >= ClosingDays.first_day,\n                Menu.menu_day <= ClosingDays.last_day\n            ).exists()\n        ).all()\n\n        for row in rows:\n            db.session.delete(row)\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass MenuItem(ModelBase):\n    __tablename__ = 'menu_item'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    menu_id = db.Column(db.Integer(), db.ForeignKey('menu.id', onupdate='CASCADE', ondelete='CASCADE'),\n                        nullable=False)\n    translatable_id = db.Column(db.Integer(), db.ForeignKey('translatable.id', onupdate='CASCADE', ondelete='RESTRICT'),\n                                nullable=False)\n    external_id = db.Column(db.Integer(), unique=True, nullable=True, server_default=expression.null())\n    course_type = db.Column(db.Enum(CourseType), nullable=False)\n    course_sub_type = db.Column(db.Enum(CourseSubType), nullable=False)\n    course_attributes = db.Column(db.Text(), nullable=False, default='[]', server_default='[]')\n    course_allergens = db.Column(db.Text(), nullable=False, default='[]', server_default='[]')\n    price_students = db.Column(db.Numeric(4, 2), nullable=False)\n    price_staff = db.Column(db.Numeric(4, 2), nullable=True)\n    data_frozen = db.Column(db.Boolean(), nullable=False, server_default=expression.false())\n\n    def __init__(self, menu: Menu, translatable_id: int, course_type: CourseType, course_sub_type: CourseSubType,\n                 price_students: Decimal, price_staff: Optional[Decimal]):\n        if not isinstance(menu, Menu):\n            raise expected('menu', menu, Menu)\n        if not isinstance(translatable_id, int):\n            raise expected('translatable_id', translatable_id, int)\n        if not isinstance(course_type, CourseType):\n            raise expected('course_type', course_type, CourseType)\n        if not isinstance(course_sub_type, CourseSubType):\n            raise expected('course_sub_type', course_sub_type, CourseSubType)\n        if not isinstance(price_students, Decimal):\n            raise expected('price_students', price_students, Decimal)\n        if price_staff is not None and not isinstance(price_staff, Decimal):\n            raise expected_or_none('price_staff', price_staff, Decimal)\n\n        self.menu = menu\n        self.translatable_id = translatable_id\n        self.course_type = course_type\n        self.course_sub_type = course_sub_type\n        self.price_students = price_students\n        self.price_staff = price_staff\n\n    def get_translation(self, language: str, translator: 'TranslationService') -> 'Translation':\n        return self.translatable.get_translation(language, translator)\n\n    @staticmethod\n    def format_price(price: Decimal) -> str:\n        if price == 0.0:\n            return ''\n        return locale.currency(price).replace(' ', '')\n\n    def get_attributes(self) -> List[CourseAttributes]:\n        # Stored as a list of strings or a list of ints (backwards compat)\n        return [CourseAttributes(v) if isinstance(v, int) else CourseAttributes[v]\n                for v in json.loads(self.course_attributes)]\n\n    def set_attributes(self, attributes: List[CourseAttributes]):\n        self.course_attributes = json.dumps([v.name for v in attributes])\n\n    def get_allergens(self) -> List[CourseAllergens]:\n        # Stored as a list of strings\n        return [CourseAllergens[v] for v in json.loads(self.course_allergens)]\n\n    def set_allergens(self, allergens: List[CourseAllergens]):\n        self.course_allergens = json.dumps([v.name for v in allergens])\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass UserDayCampusPreference(ModelBase):\n    __tablename__ = 'user_day_campus_preference'\n\n    user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id', onupdate='CASCADE', ondelete='CASCADE'),\n                        primary_key=True)\n    day = db.Column(db.Enum(Day), primary_key=True)\n    campus_id = db.Column(db.Integer(), db.ForeignKey('campus.id', onupdate='CASCADE', ondelete='CASCADE'),\n                          nullable=False)\n\n    # FIXME: Move this out of this table and instead store this in some subscription info table for daily_menu channel\n    active = db.Column(db.Boolean(), default=True, nullable=False)\n\n    def __init__(self, user_id: int, day: Day, campus_id: int, active=True) -> None:\n        if not isinstance(user_id, int):\n            raise expected('user_id', user_id, int)\n        if not isinstance(day, Day):\n            raise expected('day', day, Day)\n        if not isinstance(campus_id, int):\n            raise expected('campus_id', campus_id, int)\n        if not isinstance(active, bool):\n            raise expected('active', active, bool)\n\n        self.user_id = user_id\n        self.day = day\n        self.campus_id = campus_id\n        self.active = active\n\n    @staticmethod\n    def get_all_for_user(user: 'AppUser') -> 'List[UserDayCampusPreference]':\n        return UserDayCampusPreference.query.filter_by(user_id=user.id).all()\n\n    @staticmethod\n    def get_for_user(user: 'AppUser', day: Day) -> 'Optional[UserDayCampusPreference]':\n        return UserDayCampusPreference.query.filter_by(user_id=user.id, day=day).first()\n\n    @staticmethod\n    def create(user: 'AppUser', day: Day, campus: Campus, active=True) -> 'Optional[UserDayCampusPreference]':\n        if day in [Day.SATURDAY, Day.SUNDAY]:\n            raise ValueError('Day cannot be SATURDAY or SUNDAY')\n\n        subscription = UserDayCampusPreference(user.id, day, campus.id, active)\n\n        db.session.add(subscription)\n\n        return subscription\n\n    def __hash__(self):\n        return hash((self.user_id, self.day))\n\n\nclass AppUser(ModelBase):\n    __tablename__ = 'app_user'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    provider = db.Column(db.String(32), nullable=False)  # String ID of the provider\n    internal_id = db.Column(db.String(), nullable=False)  # ID that is specific to the provider\n    language = db.Column(db.String(5), nullable=False)\n    # Flag indicating whether a user has been informed about the new site or not\n    notified_new_site = db.Column(db.Boolean(), nullable=False, default=False, server_default=expression.false())\n    enabled = db.Column(db.Boolean(), nullable=False, default=True, server_default=expression.true())\n    data = db.Column(db.Text(), nullable=True)  # Stores data specific to the provider\n\n    __table_args__ = (\n        db.UniqueConstraint('provider', 'internal_id'),\n    )\n\n    subscriptions = db.relationship('UserDayCampusPreference', backref='user', passive_deletes=True)\n    feature_participations = db.relationship('FeatureParticipation', backref='user', passive_deletes=True)\n\n    def __init__(self, provider: str, internal_id: str, language: str):\n        if not isinstance(provider, str):\n            raise expected('provider', provider, str)\n        if not isinstance(internal_id, str):\n            raise expected('internal_id', internal_id, str)\n        if not isinstance(language, str):\n            raise expected('language', language, str)\n\n        self.provider = provider\n        self.internal_id = internal_id\n        self.language = language\n\n    def set_campus(self, day: Day, campus: Campus, active=None):\n        sub = UserDayCampusPreference.get_for_user(self, day)\n        if sub is None:\n            UserDayCampusPreference.create(self, day, campus, active=True if active is None else active)\n        else:\n            sub.campus = campus\n            if active is not None:\n                sub.active = active\n\n    def set_day_active(self, day: Day, active: bool):\n        sub = UserDayCampusPreference.get_for_user(self, day)\n        if sub is None:\n            if active:\n                raise ValueError('Cannot set subscription active if there is no campus set')\n        else:\n            sub.active = active\n\n    def get_campus(self, day: Day) -> 'Optional[Campus]':\n        sub = UserDayCampusPreference.get_for_user(self, day)\n        if sub is not None:\n            return sub.campus\n        else:\n            return None\n\n    def get_subscription(self, day: Day) -> 'Optional[UserDayCampusPreference]':\n        return UserDayCampusPreference.get_for_user(self, day)\n\n    def set_language(self, language: str):\n        self.language = language\n\n    def set_active(self, day: Day, active: bool):\n        sub = UserDayCampusPreference.get_for_user(self, day)\n        if sub is None:\n            raise ValueError('User does not have a subscription on day {}'.format(day.name))\n\n        sub.active = active\n\n    @staticmethod\n    def create(provider: str, internal_id: str, language: str) -> 'AppUser':\n        user = AppUser(provider, internal_id, language)\n\n        db.session.add(user)\n\n        return user\n\n    def delete(self):\n        db.session.delete(self)\n\n    @staticmethod\n    def find_subscribed_users_by_day(day: Day, provider=None) -> 'List[AppUser]':\n        q = AppUser.query\n        if provider:\n            q = q.filter_by(provider=provider)\n\n        return q.join(AppUser.subscriptions).filter(db.and_(UserDayCampusPreference.day == day,\n                                                            UserDayCampusPreference.active == expression.true(),\n                                                            AppUser.enabled == expression.true()\n                                                            )).order_by(AppUser.provider, AppUser.internal_id).all()\n\n    @staticmethod\n    def find_by_id(provider: str, internal_id: str) -> 'Optional[AppUser]':\n        return AppUser.query.filter_by(provider=provider, internal_id=internal_id).first()\n\n    @staticmethod\n    def find_by_provider(provider: str) -> 'List[AppUser]':\n        return AppUser.query.filter_by(provider=provider).order_by(AppUser.internal_id).all()\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass Feature(ModelBase):\n    __tablename__ = 'feature'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    string_id = db.Column(db.String(256), nullable=False, unique=True)\n    description = db.Column(db.Text())\n    globally_available = db.Column(db.Boolean(), default=False, nullable=False)\n\n    participations = db.relationship('FeatureParticipation', backref='feature', passive_deletes=True)\n\n    def __init__(self, string_id: str, description: str = None, globally_available=False):\n        if not isinstance(string_id, str):\n            raise expected('string_id', string_id, str)\n        if description is not None and not isinstance(description, str):\n            raise expected_or_none('description', description, str)\n        if globally_available is not None and not isinstance(globally_available, bool):\n            raise expected_or_none('globally_available', globally_available, bool)\n\n        self.string_id = string_id\n        self.description = description\n        self.globally_available = globally_available\n\n    @staticmethod\n    def create(string_id: str, description: str = None, globally_available=False) -> 'Optional[Feature]':\n        feature = Feature(string_id, description, globally_available)\n\n        db.session.add(feature)\n\n        return feature\n\n    @staticmethod\n    def find_by_id(string_id: str) -> 'Optional[Feature]':\n        return Feature.query.filter_by(string_id=string_id).first()\n\n    @staticmethod\n    def get_all() -> 'List[Feature]':\n        return Feature.query.all()\n\n    @staticmethod\n    def is_user_participating(user: Optional[AppUser], string_id: str) -> bool:\n        feature = Feature.find_by_id(string_id)\n        if feature is None:\n            return False\n\n        if feature.globally_available:\n            return True\n\n        if user is None:\n            return False\n\n        return FeatureParticipation.get_for_user(user, feature) is not None\n\n    @staticmethod\n    def set_user_participating(user: AppUser, string_id: str, participating: bool):\n        feature = Feature.find_by_id(string_id)\n        participation = FeatureParticipation.get_for_user(user, feature)\n\n        if participating:\n            if not participation:\n                FeatureParticipation.create(user, feature)\n        else:\n            if participation:\n                db.session.delete(feature)\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass FeatureParticipation(ModelBase):\n    __tablename__ = 'feature_participation'\n\n    user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id', onupdate='CASCADE', ondelete='CASCADE'),\n                        primary_key=True)\n    feature_id = db.Column(db.Integer(), db.ForeignKey('feature.id', onupdate='CASCADE', ondelete='CASCADE'),\n                           primary_key=True)\n\n    def __init__(self, user_id: int, feature_id: int):\n        if not isinstance(user_id, int):\n            raise expected('user_id', user_id, int)\n        if not isinstance(feature_id, int):\n            raise expected('feature_id', feature_id, int)\n\n        self.user_id = user_id\n        self.feature_id = feature_id\n\n    @staticmethod\n    def create(user: AppUser, feature: Feature) -> 'Optional[FeatureParticipation]':\n        participation = FeatureParticipation(user.id, feature.id)\n\n        db.session.add(participation)\n\n        return participation\n\n    @staticmethod\n    def get_for_user(user: AppUser, feature: Feature) -> 'Optional[FeatureParticipation]':\n        return FeatureParticipation.query.filter_by(user_id=user.id, feature_id=feature.id).first()\n\n    def __hash__(self):\n        return hash((self.user_id, self.feature_id))\n\n\ndef recreate_db():\n    db.drop_all()\n    db.create_all()\n    db.session.commit()\n\n\n# noinspection PyUnusedLocal\ndef create_standard_values():\n    cst = Campus.create('Stadscampus', 'cst', ['stad', 'stadscampus'], 1)\n    cde = Campus.create('Campus Drie Eiken', 'cde', ['drie', 'eiken'], 2)\n    cmi = Campus.create('Campus Middelheim', 'cmi', ['middelheim'], 3)\n    cgb = Campus.create('Campus Groenenborger', 'cgb', ['groenenborger'], 4)\n    cmu = Campus.create('Campus Mutsaard', 'cmu', ['mutsaard'], 5)\n    hzs = Campus.create('Hogere Zeevaartschool', 'hzs', ['hogere', 'zeevaartschool'], 6)\n    hzs.active = False\n    db.session.commit()\n\n\ndef import_dump(dump_file):\n    campus_dict: Dict[str, Campus] = dict()\n\n    def get_campus(short_name) -> Campus:\n        if short_name not in campus_dict:\n            campus_dict[short_name] = Campus.get_by_short_name(short_name)\n        return campus_dict[short_name]\n\n    with open(dump_file) as file:\n        _ = file.readline()  # Skip header\n\n        line = file.readline()\n        while line:\n            line = line.strip()\n            split = list(line.split('\\t'))\n\n            if len(split) == 8:\n                split[1] = split[1] == 'True'\n            if split[7] == '0':\n                split[7] = ''  # Query locale\n\n            user = AppUser.create('facebook', split[0], split[7])\n            user.set_campus(Day.MONDAY, get_campus(split[2]), active=split[1])\n            user.set_campus(Day.TUESDAY, get_campus(split[3]), active=split[1])\n            user.set_campus(Day.WEDNESDAY, get_campus(split[4]), active=split[1])\n            user.set_campus(Day.THURSDAY, get_campus(split[5]), active=split[1])\n            user.set_campus(Day.FRIDAY, get_campus(split[6]), active=split[1])\n\n            db.session.add(user)\n\n            line = file.readline()\n\n        db.session.commit()\n"
  },
  {
    "path": "komidabot/models_training.py",
    "content": "import datetime\nimport enum\nimport json\nfrom typing import Any, List, NamedTuple, Optional, TypedDict, Union\n\nfrom sqlalchemy.sql import expression\n\nfrom extensions import db, ModelBase\nfrom komidabot.models_users import RegisteredUser\nfrom komidabot.util import expected\n\n# ChoiceSchemaType = NamedTuple('ChoiceType', (('display', str), ('value', Any),))\n#\n#\n# class SchemaElementType(enum.Enum):\n#     STATIC_TEXT = 1  # Value type: str; always readonly\n#     STATIC_IMAGE = 2  # Value type: str (base64 encoded data); always readonly\n#     DIVIDER = 3  # Value type: nothing; always readonly\n#     BOOLEAN = 4  # Value type: nothing\n#     CHOICE = 5  # Value type: List[ChoiceType]\n#     MULTIPLE_CHOICE = 6  # Value type: List[ChoiceType]\n#     TEXT = 7  # Value type: nothing\n#     NUMBER = 8  # Value type: nothing\n#\n#\n# class SchemaElement(TypedDict):\n#     type: int  # SchemaElementType\n#     description: Optional[str]\n#     readonly: Optional[bool]\n#\n#\n# DataElement = Union[str, List[ChoiceSchemaType], None]\n#\n#\n# class TrainingSchema(ModelBase):\n#     __tablename__ = 'training_schema'\n#\n#     id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n#     name = db.Column(db.String(), nullable=False)\n#     schema = db.Column(db.String(), nullable=False)\n#\n#     def __init__(self, name: str, schema: str):\n#         if not isinstance(name, str):\n#             raise expected('name', name, str)\n#         if not isinstance(schema, str):\n#             raise expected('schema', schema, str)\n#\n#         self.name = name\n#         self.schema = schema\n#\n#     @staticmethod\n#     def create(name: str, schema: 'List[SchemaElement]', add_to_db=True) -> 'TrainingSchema':\n#         if not isinstance(schema, list):\n#             raise expected('schema', schema, list)\n#         for element in schema:\n#             if not isinstance(element, dict):\n#                 raise expected('schema[]', element, dict)\n#             if 'type' not in element:\n#                 raise ValueError('Missing type in SchemaElement')\n#\n#         # FIXME: Verify schema\n#         result = TrainingSchema(name, json.dumps(schema))\n#\n#         if add_to_db:\n#             db.session.add(result)\n#\n#         return result\n#\n#     @staticmethod\n#     def find_by_id(schema_id: int) -> 'Optional[TrainingSchema]':\n#         return TrainingSchema.query.filter_by(id=schema_id).first()\n#\n#     def get_schema(self) -> 'List[SchemaElement]':\n#         return json.loads(self.schema)\n#\n#     def add_input(self, data: 'List[DataElement]',\n#                   add_to_db=True) -> 'Optional[TrainingInput]':\n#         # FIXME: Verify data\n#         result = TrainingInput(self.id, json.dumps(data))\n#\n#         if add_to_db:\n#             db.session.add(result)\n#\n#         return result\n#\n#\n# class TrainingInput(ModelBase):\n#     __tablename__ = 'training_input'\n#\n#     id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n#     schema_id = db.Column(db.Integer(), db.ForeignKey('training_schema.id'), nullable=False)\n#     data = db.Column(db.String(), nullable=False)\n#\n#     def __init__(self, schema_id: int, data: str):\n#         if not isinstance(schema_id, int):\n#             raise expected('schema_id', schema_id, int)\n#         if not isinstance(data, str):\n#             raise expected('data', data, str)\n#\n#         self.schema_id = schema_id\n#         self.data = data\n#\n#     @staticmethod\n#     def find_by_id(input_id: int) -> 'Optional[TrainingInput]':\n#         return TrainingInput.query.filter_by(id=input_id).first()\n#\n#     @staticmethod\n#     def get_random(user: 'RegisteredUser') -> 'Optional[TrainingInput]':\n#         return TrainingInput.query.order_by(expression.func.random()).filter(\n#             expression.not_(\n#                 TrainingResponse.query.filter(\n#                     TrainingInput.id == TrainingResponse.input_id,\n#                     TrainingResponse.user_id == user.id\n#                 ).exists()\n#             )\n#         ).first()\n#\n#     def add_response(self, user: 'RegisteredUser', data: Any, add_to_db=True):\n#         # FIXME: Verify data\n#         result = TrainingResponse(self.id, user.id, json.dumps(data))\n#\n#         if add_to_db:\n#             db.session.add(result)\n#\n#         return result\n#\n#\n# class TrainingResponse(ModelBase):\n#     __tablename__ = 'training_response'\n#\n#     id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n#     input_id = db.Column(db.Integer(), db.ForeignKey('training_input.id'), nullable=False)\n#     user_id = db.Column(db.Integer(), db.ForeignKey('registered_users.id', onupdate='CASCADE', ondelete='CASCADE'),\n#                         nullable=False)\n#     data = db.Column(db.String(), nullable=False)\n#\n#     def __init__(self, input_id: int, user_id: int, data: str):\n#         if not isinstance(input_id, int):\n#             raise expected('input_id', input_id, int)\n#         if not isinstance(user_id, int):\n#             raise expected('user_id', user_id, int)\n#         if not isinstance(data, str):\n#             raise expected('data', data, str)\n#\n#         self.input_id = input_id\n#         self.user_id = user_id\n#         self.data = data\n\n\nclass LearningDatapoint(ModelBase):\n    __tablename__ = 'learning_datapoint'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    campus_id = db.Column(db.Integer(), db.ForeignKey('campus.id'), nullable=False)\n    menu_day = db.Column(db.Date(), nullable=False)\n    screenshot = db.Column(db.Text(), nullable=False)\n    processed_data = db.Column(db.Text(), nullable=False)\n\n    submissions = db.relationship('LearningDatapointSubmission', backref='datapoint', passive_deletes=True)\n\n    def __init__(self, campus_id: int, menu_day: datetime.date, screenshot: str, processed_data: Any):\n        if not isinstance(campus_id, int):\n            raise expected('campus_id', campus_id, int)\n        if not isinstance(menu_day, datetime.date):\n            raise expected('menu_day', menu_day, datetime.date)\n        if screenshot is None:\n            raise ValueError('screenshot expected not None')\n        if processed_data is None:\n            raise ValueError('processed_data expected not None')\n\n        self.campus_id = campus_id\n        self.menu_day = menu_day\n        self.screenshot = screenshot\n        self.processed_data = json.dumps(processed_data)\n\n    @staticmethod\n    def create(campus: 'Campus', menu_day: datetime.date, screenshot: str,\n               processed_data: Any) -> 'Optional[LearningDatapoint]':\n        datapoint = LearningDatapoint(campus.id, menu_day, screenshot, processed_data)\n\n        db.session.add(datapoint)\n\n        return datapoint\n\n    @staticmethod\n    def find_by_id(datapoint_id: int) -> 'Optional[LearningDatapoint]':\n        return LearningDatapoint.query.filter_by(id=datapoint_id).first()\n\n    @staticmethod\n    def get_all() -> 'List[LearningDatapoint]':\n        return LearningDatapoint.query.all()\n\n    @staticmethod\n    def get_random(user: 'RegisteredUser') -> 'Optional[LearningDatapoint]':\n        return LearningDatapoint.query.order_by(expression.func.random()).filter(\n            expression.not_(\n                LearningDatapointSubmission.query.filter(\n                    LearningDatapoint.id == LearningDatapointSubmission.datapoint_id,\n                    LearningDatapointSubmission.user_id == user.id\n                ).exists()\n            )\n        ).first()\n\n    def user_submit(self, user: 'RegisteredUser', submission_data: Any):\n        LearningDatapointSubmission.create(self, user, submission_data)\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass LearningDatapointSubmission(ModelBase):\n    __tablename__ = 'learning_datapoint_submission'\n\n    user_id = db.Column(db.Integer(),\n                        db.ForeignKey('registered_users.id', onupdate='CASCADE', ondelete='CASCADE'),\n                        primary_key=True)\n    datapoint_id = db.Column(db.Integer(),\n                             db.ForeignKey('learning_datapoint.id', onupdate='CASCADE', ondelete='CASCADE'),\n                             primary_key=True)\n    submission_data = db.Column(db.Text(), nullable=False)\n\n    def __init__(self, user_id: int, datapoint_id: int, submission_data: Any):\n        if not isinstance(user_id, int):\n            raise expected('user_id', user_id, int)\n        if not isinstance(datapoint_id, int):\n            raise expected('datapoint_id', datapoint_id, int)\n        if submission_data is None:\n            raise ValueError('submission_data expected not None')\n\n        self.user_id = user_id\n        self.datapoint_id = datapoint_id\n        self.submission_data = json.dumps(submission_data)\n\n    @staticmethod\n    def create(datapoint: LearningDatapoint, user: 'RegisteredUser',\n               submission_data: Any) -> 'Optional[LearningDatapointSubmission]':\n        submission = LearningDatapointSubmission(user.id, datapoint.id, submission_data)\n\n        db.session.add(submission)\n\n        return submission\n\n    def __hash__(self):\n        return hash((self.user_id, self.datapoint_id))\n"
  },
  {
    "path": "komidabot/models_users.py",
    "content": "import json\nfrom typing import Dict, List, Optional, TypedDict, Union\n\nfrom flask_login import UserMixin\nfrom sqlalchemy.sql import functions\n\nfrom extensions import db, ModelBase, Table\nfrom komidabot.util import expected\n\n\nclass AdminSubscription(TypedDict):\n    endpoint: str  # XXX: This is a globally unique identifier for the client\n    keys: Dict[str, str]\n\n\nuser_roles_table = Table(\n    'user_roles', ModelBase.metadata,\n    db.Column('user_id', db.Integer(), db.ForeignKey('registered_users.id', ondelete='CASCADE'), primary_key=True),\n    db.Column('role_id', db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True)\n)\n\n\nclass RegisteredUser(ModelBase, UserMixin):\n    __tablename__ = 'registered_users'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n\n    provider = db.Column(db.String(16), nullable=False)\n    subject = db.Column(db.String(), nullable=False)\n    name = db.Column(db.String(), nullable=False)\n    email = db.Column(db.String(), nullable=False, unique=True)\n    profile_picture = db.Column(db.String(), nullable=False)\n\n    registered_on = db.Column(db.DateTime(), nullable=False, server_default=functions.now())\n    activated_on = db.Column(db.DateTime(), nullable=True)\n\n    web_subscriptions = db.Column(db.String(), nullable=False, server_default='[]')\n\n    roles: 'List[Role]' = db.relationship('Role', secondary=user_roles_table, back_populates='users')\n    submissions = db.relationship('LearningDatapointSubmission', backref='registered_user', passive_deletes=True)\n\n    __table_args__ = (\n        db.UniqueConstraint('provider', 'subject'),\n    )\n\n    def __init__(self, provider: str, subject: str, name: str, email: str, profile_picture: str):\n        if not isinstance(provider, str):\n            raise expected('provider', provider, str)\n        if not isinstance(subject, str):\n            raise expected('subject', subject, str)\n        if not isinstance(name, str):\n            raise expected('name', name, str)\n        if not isinstance(email, str):\n            raise expected('email', email, str)\n        if not isinstance(profile_picture, str):\n            raise expected('profile_picture', profile_picture, str)\n\n        self.provider = provider\n        self.subject = subject\n        self.name = name\n        self.email = email\n        self.profile_picture = profile_picture\n\n    @staticmethod\n    def create(provider: str, subject: str, name: str, email: str, profile_picture: str,\n               add_to_db=True) -> 'RegisteredUser':\n        user = RegisteredUser(provider, subject, name, email, profile_picture)\n\n        if add_to_db:\n            db.session.add(user)\n\n        return user\n\n    def delete(self):\n        db.session.delete(self)\n\n    # Overrides UserMixin.is_active\n    @property\n    def is_active(self):\n        return self.activated_on is not None\n\n    # Query methods\n    @staticmethod\n    def get_by_id(user_id: int) -> 'Optional[RegisteredUser]':\n        return RegisteredUser.query.filter_by(id=user_id).first()\n\n    @staticmethod\n    def find_by_provider_id(provider: str, subject: str) -> 'Optional[RegisteredUser]':\n        return RegisteredUser.query.filter_by(provider=provider, subject=subject).first()\n\n    @staticmethod\n    def find_by_email(email: str) -> 'Optional[RegisteredUser]':\n        return RegisteredUser.query.filter_by(email=email).first()\n\n    @staticmethod\n    def get_all() -> 'List[RegisteredUser]':\n        return RegisteredUser.query.all()\n\n    @staticmethod\n    def get_all_active() -> 'List[RegisteredUser]':\n        return RegisteredUser.query.filter(RegisteredUser.activated_on != None).all()\n\n    @staticmethod\n    def get_all_by_role(role: 'Role') -> 'List[RegisteredUser]':\n        return role.users\n        # return RegisteredUser.query.filter(\n        #     UserRoles.user_id == RegisteredUser.id,\n        #     UserRoles.role_id == role.id\n        # ).all()\n\n    # Roles functions\n    def get_roles(self) -> 'List[Role]':\n        return self.roles\n\n    def add_role(self, role: 'Role'):\n        self.roles.append(role)\n\n    def remove_role(self, role: 'Role'):\n        self.roles.remove(role)\n\n    def is_role(self, role: 'Union[str, Role]') -> bool:\n        if isinstance(role, str):\n            role = Role.find_by_name(role)\n            return role is not None and role in self.roles\n        elif isinstance(role, Role):\n            return role in self.roles\n        else:\n            raise ValueError('role')\n\n    # Subscriptions functions\n    def get_subscriptions(self) -> 'List[AdminSubscription]':\n        return json.loads(self.web_subscriptions)\n\n    def set_subscriptions(self, subscriptions: 'List[AdminSubscription]'):\n        self.web_subscriptions = json.dumps(subscriptions)\n\n    def add_subscription(self, endpoint: str, keys: Dict[str, str]):\n        subscriptions: 'List[AdminSubscription]' = []\n        found = False\n\n        for sub in self.get_subscriptions():\n            subscriptions.append(sub)\n\n            if sub['endpoint'] == endpoint:\n                found = True\n\n        if not found:\n            subscriptions.append({'endpoint': endpoint, 'keys': keys})\n\n        self.set_subscriptions(subscriptions)\n\n    def remove_subscription(self, endpoint: str):\n        self.set_subscriptions([sub for sub in self.get_subscriptions() if sub['endpoint'] != endpoint])\n\n    @staticmethod\n    def replace_subscription(old_endpoint: str, endpoint: str, keys: Dict[str, str]):\n        for user in RegisteredUser.get_all():\n            user.set_subscriptions([sub if sub['endpoint'] != old_endpoint else {'endpoint': endpoint, 'keys': keys}\n                                    for sub in user.get_subscriptions()])\n\n    def __hash__(self):\n        return hash(self.id)\n\n\nclass Role(ModelBase):\n    __tablename__ = 'roles'\n\n    id = db.Column(db.Integer(), primary_key=True, autoincrement=True)\n    name = db.Column(db.String(64), nullable=False, unique=True)\n\n    users = db.relationship('RegisteredUser', secondary=user_roles_table, back_populates='roles')\n\n    def __init__(self, name: str):\n        if not isinstance(name, str):\n            raise expected('name', name, str)\n\n        self.name = name\n\n    @staticmethod\n    def create(name: str, add_to_db=True) -> 'Role':\n        user = Role(name)\n\n        if add_to_db:\n            db.session.add(user)\n\n        return user\n\n    @staticmethod\n    def find_by_name(name: str) -> 'Optional[Role]':\n        return Role.query.filter_by(name=name).first()\n\n"
  },
  {
    "path": "komidabot/rate_limit.py",
    "content": "import time\nfrom collections import deque\nfrom datetime import datetime\n\n\nclass Limiter:\n    def __init__(self, max_rate: int):\n        self.max_rate = max_rate\n        self.last_times = deque()\n\n    def __call__(self):\n        now = datetime.now()\n\n        if len(self.last_times) < self.max_rate:\n            self.last_times.append(now)\n            return\n\n        delta = (now - self.last_times.popleft()).total_seconds()\n\n        if delta < 1:\n            time.sleep(1.0 - delta)\n\n        self.last_times.append(now)\n"
  },
  {
    "path": "komidabot/subscriptions/__init__.py",
    "content": "from typing import Dict, List, Optional, Union\n\nfrom komidabot.messages import Message\nfrom komidabot.users import User\n\n__all__ = ['SubscriptionChannel', 'SubscriptionManager']\n\n\nclass SubscriptionQuery:\n    pass\n\n\nclass SubscriptionData:\n    pass\n\n\nclass SubscriptionChannel:\n    def user_supported(self, user: 'User') -> bool:\n        return user.supports_subscription_channel(self.get_name()) and user.is_reachable()\n\n    def get_subscribed_users(self, /, query: Union[SubscriptionQuery, Dict] = None) -> 'List[User]':\n        raise NotImplementedError()\n\n    def get_query_from(self, query: Dict = None) -> Optional[SubscriptionQuery]:\n        raise NotImplementedError()\n\n    def deliver_message(self, message: Message):\n        raise NotImplementedError()\n\n    def get_name(self) -> str:\n        raise NotImplementedError()\n\n    def user_subscribe(self, user: 'User', /, data: SubscriptionData = None) -> bool:\n        raise NotImplementedError()\n\n    def user_unsubscribe(self, user: 'User') -> bool:\n        raise NotImplementedError()\n\n    def user_subscription_data(self, user: 'User') -> Optional[SubscriptionData]:\n        raise NotImplementedError()\n\n\nclass SubscriptionManager:\n    def __init__(self):\n        self._channels: 'Dict[str, SubscriptionChannel]' = dict()\n\n    def register_channel(self, channel: 'SubscriptionChannel'):\n        if channel.get_name() in self._channels:\n            raise ValueError('Duplicate channel name registered')\n\n        self._channels[channel.get_name()] = channel\n\n    def get_channel(self, channel: str) -> 'Optional[SubscriptionChannel]':\n        return self._channels.get(channel, None)\n\n    def get_subscribed_users(self, channel: str, /, query: Union[SubscriptionQuery, Dict] = None) -> 'List[User]':\n        if channel not in self._channels:\n            raise ValueError('Unknown channel')\n\n        channel_obj = self._channels[channel]\n\n        return channel_obj.get_subscribed_users(query=query)\n\n    def deliver_message(self, channel: str, message: Message):\n        if channel not in self._channels:\n            raise ValueError('Unknown channel')\n\n        return self._channels[channel].deliver_message(message)\n\n    def user_subscribe(self, user: 'User', channel: str, /, data: SubscriptionData = None) -> bool:\n        if channel not in self._channels:\n            raise ValueError('Unknown channel')\n\n        channel_obj = self._channels[channel]\n\n        if not channel_obj.user_supported(user):\n            return False\n\n        return channel_obj.user_subscribe(user, data=data)\n\n    def user_unsubscribe(self, user: 'User', channel: str) -> bool:\n        if channel not in self._channels:\n            raise ValueError('Unknown channel')\n\n        channel_obj = self._channels[channel]\n\n        if not channel_obj.user_supported(user):\n            return False\n\n        return channel_obj.user_unsubscribe(user)\n\n    def user_subscription_data(self, user: 'User', channel: str) -> Optional[SubscriptionData]:\n        if channel not in self._channels:\n            raise ValueError('Unknown channel')\n\n        channel_obj = self._channels[channel]\n\n        if not channel_obj.user_supported(user):\n            return None\n\n        return channel_obj.user_subscription_data(user)\n"
  },
  {
    "path": "komidabot/subscriptions/daily_menu.py",
    "content": "from typing import Dict, List, Optional, Union\n\nimport komidabot.messages as messages\nimport komidabot.models as models\nimport komidabot.subscriptions as subscriptions\nfrom extensions import db\nfrom komidabot.app import get_app\nfrom komidabot.messages import Message\nfrom komidabot.models import Day\nfrom komidabot.users import User\n\n__all__ = ['CHANNEL_ID', 'Channel']\n\nCHANNEL_ID = 'daily_menu'\n\n\nclass Query(subscriptions.SubscriptionQuery):\n    def __init__(self, day: models.Day, campus: models.Campus = None):\n        self.day = day\n        self.campus = campus\n\n\nclass Data(subscriptions.SubscriptionData):\n    class Day:\n        def __init__(self):\n            self.campus = None\n            self.active = False\n\n    def __init__(self):\n        self.monday = Data.Day()\n        self.tuesday = Data.Day()\n        self.wednesday = Data.Day()\n        self.thursday = Data.Day()\n        self.friday = Data.Day()\n\n        self.days = [self.monday, self.tuesday, self.wednesday, self.thursday, self.friday]\n\n\nclass Channel(subscriptions.SubscriptionChannel):\n    def get_subscribed_users(self, /, query: Union[Query, Dict] = None) -> 'List[User]':\n        if not isinstance(query, Query):\n            query = self.get_query_from(query)\n\n        assert isinstance(query, Query), 'query must be SubscriptionQuery'\n\n        if query.campus is not None:\n            raise NotImplementedError('Cannot query by (day, campus) right now')\n\n        app = get_app()\n        user_manager = app.user_manager\n\n        users = models.AppUser.find_subscribed_users_by_day(query.day)\n\n        return [user for user in (user_manager.get_user(user) for user in users) if self.user_supported(user)]\n\n    def get_query_from(self, query: Dict = None) -> Optional[Query]:\n        if query is None:\n            return None\n        return Query(day=query.get('day'), campus=query.get('campus', None))\n\n    def deliver_message(self, message: Message):\n        if not isinstance(message, messages.SubscriptionMenuMessage):\n            raise NotImplementedError('Daily menu channel only supports SubscriptionMenuMessage')\n\n        day = Day(message.date.isoweekday())\n        changed = False\n\n        for user in self.get_subscribed_users(query=Query(day)):\n            if user.send_message_or_remove(CHANNEL_ID, message):\n                changed = True\n\n        if changed:\n            db.session.commit()\n\n    def get_name(self):\n        return CHANNEL_ID\n\n    def user_subscribe(self, user: 'User', /, data: Data = None) -> bool:\n        return False\n\n    def user_unsubscribe(self, user: 'User') -> bool:\n        return False\n\n    def user_subscription_data(self, user: 'User') -> Optional[Data]:\n        result = Data()\n        return None  # This subscription doesn't take data\n"
  },
  {
    "path": "komidabot/translation.py",
    "content": "from googletrans import Translator\n\nLanguage = str\n\nLANGUAGE_DUTCH = 'nl'\nLANGUAGE_ENGLISH = 'en'\nLANGUAGE_FRENCH = 'fr'\n\n\ndef _fix_language(language: Language):\n    if language == 'zh_CN' or language == 'zh_SG':\n        return 'zh-cn'\n    elif language == 'zh_HK' or language == 'zh_TW':\n        return 'zh-tw'\n\n    return language\n\n\nclass TranslationService:\n    def translate(self, text: str, from_language: Language, to_language: Language):\n        \"\"\"\n        Submit a string to be translated.\n        :param text: The string to translate\n        :param from_language: A 2 letter string defining the language to translate from\n        :param to_language: A 2 letter string defining the language to translate to\n        :return: The translated string\n        \"\"\"\n        raise NotImplementedError()\n\n    @property\n    def identifier(self):\n        raise NotImplementedError()\n\n    @property\n    def pretty_name(self):\n        raise NotImplementedError()\n\n\nclass KomidaTranslationService(TranslationService):\n    def translate(self, text: str, from_language: Language, to_language: Language):\n        raise Exception('Komida translator service is a placeholder and cannot translate')\n\n    @property\n    def identifier(self):\n        return 'komida'\n\n    @property\n    def pretty_name(self):\n        return 'Komida'\n\n\nclass GoogleTranslationService(TranslationService):\n    def __init__(self):\n        self.translator = Translator()\n\n    def translate(self, text: str, from_language: Language, to_language: Language):\n        return self.translator.translate(text, src=from_language, dest=to_language).text\n\n    @property\n    def identifier(self):\n        return 'google'\n\n    @property\n    def pretty_name(self):\n        return 'Google Translate'\n\n\nclass BingTranslationService(TranslationService):\n    def translate(self, text: str, from_language: Language, to_language: Language):\n        raise NotImplementedError()\n\n    @property\n    def identifier(self):\n        return 'bing'\n\n    @property\n    def pretty_name(self):\n        return 'Bing Translate'\n"
  },
  {
    "path": "komidabot/triggers.py",
    "content": "import datetime\n\nimport komidabot.users as users\nfrom komidabot.messages import Aspect, Trigger\n\n\nclass SubscriptionTrigger(Trigger):\n    def __init__(self, *args, date: datetime.date = None, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.date = date\n\n    def get_repr_text(self):\n        return ['SubscriptionTrigger', '- Date: ' + repr(self.date)]\n\n\nclass TextTrigger(Trigger):\n    def __init__(self, text, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.text = text\n\n    def get_repr_text(self):\n        return ['TextTrigger', '- Text: ' + repr(self.text)]\n\n\nclass NewUserAspect(Aspect):\n    def __repr__(self):\n        return 'NewUserAspect()'\n\n\nclass SenderAspect(Aspect):\n    def __init__(self, sender: users.User):\n        super().__init__()\n        self.sender = sender\n\n    def __repr__(self):\n        return 'SenderAspect({})'.format(repr(self.sender))\n\n\nclass AtAdminAspect(Aspect):\n    def __repr__(self):\n        return 'AtAdminAspect()'\n\n\nclass DatetimeAspect(Aspect):\n    allows_multiple = True\n\n    def __init__(self, value: str, grain: str):\n        super().__init__()\n        self.value = value\n        self.grain = grain\n\n    def __repr__(self):\n        return 'DatetimeAspect({}, {})'.format(repr(self.value), self.grain)\n\n\nclass LocaleAspect(Aspect):\n    def __init__(self, locale: str, confidence: float):\n        super().__init__()\n        self.locale = locale\n        self.confidence = confidence\n\n    def __repr__(self):\n        return 'LocaleAspect({}, {})'.format(self.locale, self.confidence)\n"
  },
  {
    "path": "komidabot/users.py",
    "content": "import datetime\nimport functools\nimport json\nfrom typing import Dict, List, Optional, Union\nfrom typing import NamedTuple\n\nimport komidabot.messages as messages\nimport komidabot.models as models\nfrom komidabot.app import get_app\n\n__all__ = ['UnifiedUserManager', 'User', 'UserId', 'UserManager']\n\n\nclass UserId(NamedTuple):\n    id: str\n    provider: str\n\n    def __repr__(self):\n        return '{}/{}'.format(self.provider, self.id)\n\n\nclass UserManager:  # TODO: This probably could use more methods\n    def get_user(self, user: 'Union[UserId, models.AppUser]', **kwargs) -> 'User':\n        raise NotImplementedError()\n\n    def get_administrators(self) -> 'List[User]':\n        identifier = self.get_identifier()\n\n        return [self.get_user(user) for user in get_app().admin_ids if user.provider == identifier]\n\n    def initialise(self):\n        raise NotImplementedError()\n\n    def get_identifier(self) -> str:\n        raise NotImplementedError()\n\n\nclass User:\n    @property\n    def id(self) -> UserId:\n        return UserId(self.get_internal_id(), self.get_provider_name())\n\n    def get_provider_name(self) -> 'str':\n        raise NotImplementedError()\n\n    @property\n    def manager(self) -> UserManager:\n        return self.get_manager()\n\n    def get_manager(self) -> UserManager:\n        raise NotImplementedError()\n\n    def get_internal_id(self) -> 'str':\n        raise NotImplementedError()\n\n    def get_db_user(self) -> 'Optional[models.AppUser]':\n        user_id = self.id\n        return models.AppUser.find_by_id(user_id.provider, user_id.id)\n\n    def add_to_db(self):\n        user_id = self.id\n        models.AppUser.create(user_id.provider, user_id.id, '')\n\n    def remove_from_db(self):\n        \"\"\"\n        Deletes the user from the database.\n        \"\"\"\n        user = self.get_db_user()\n        if user is None:\n            return\n\n        user.delete()\n\n    def get_locale(self) -> 'Optional[str]':  # TODO: Properly look into this\n        user = self.get_db_user()\n        if user is None:\n            return None\n\n        return user.language\n\n    def get_is_notified_new_site(self) -> 'Optional[bool]':\n        user = self.get_db_user()\n        if user is None:\n            return None\n\n        return user.notified_new_site\n\n    def set_is_notified_new_site(self, value: bool):\n        user = self.get_db_user()\n        if user is None:\n            return\n\n        user.notified_new_site = value\n\n    def get_campus_for_day(self, date: Union[models.Day, datetime.date]) -> 'Optional[models.Campus]':\n        user = self.get_db_user()\n        if user is None:\n            return None\n\n        if isinstance(date, datetime.date):\n            day = models.Day(date.isoweekday())\n        elif isinstance(date, models.Day):\n            day = date\n        else:\n            raise ValueError('date')\n\n        return user.get_campus(day)\n\n    def set_campus_for_day(self, campus: models.Campus, date: Union[models.Day, datetime.date]):\n        user = self.get_db_user()\n        if user is None:\n            return\n\n        if isinstance(date, datetime.date):\n            day = models.Day(date.isoweekday())\n        elif isinstance(date, models.Day):\n            day = date\n        else:\n            raise ValueError('date')\n\n        sub = user.get_subscription(day)\n\n        if sub is None:\n            # Make new subscription and set it to enabled by default\n            user.set_campus(day, campus, True)\n        else:\n            user.set_campus(day, campus)\n\n    def disable_subscription_for_day(self, date: Union[models.Day, datetime.date]) -> bool:\n        user = self.get_db_user()\n        if user is None:\n            return False\n\n        if isinstance(date, datetime.date):\n            day = models.Day(date.isoweekday())\n        elif isinstance(date, models.Day):\n            day = date\n        else:\n            raise ValueError('date')\n\n        sub = user.get_subscription(day)\n\n        if sub is not None and sub.active:\n            sub.active = False\n            return True\n        return False\n\n    def get_subscription_for_day(self, date: Union[models.Day, datetime.date]) \\\n            -> 'Optional[models.UserDayCampusPreference]':\n        user = self.get_db_user()\n        if user is None:\n            return None\n\n        if isinstance(date, datetime.date):\n            day = models.Day(date.isoweekday())\n        elif isinstance(date, models.Day):\n            day = date\n        else:\n            raise ValueError('date')\n\n        return user.get_subscription(day)\n\n    def mark_reachable(self) -> bool:\n        \"\"\"\n        Ensures the user is marked as being reachable.\n        :return: True if the user was marked unreachable before, False otherwise.\n        \"\"\"\n        user = self.get_db_user()\n        if user is None:\n            return False\n\n        if not user.enabled:\n            user.enabled = True\n            return True\n\n        return False\n\n    def mark_unreachable(self):\n        \"\"\"\n        Marks the user as being unreachable, effectively disabling subscription messages from going through.\n        \"\"\"\n        user = self.get_db_user()\n        if user is None:\n            return\n\n        user.enabled = False\n\n    def is_reachable(self) -> bool:\n        \"\"\"\n        Checks whether the user is reachable or not.\n        :return: True if the user is reachable, False otherwise.\n        \"\"\"\n        user = self.get_db_user()\n        if user is None:\n            return False\n\n        return user.enabled\n\n    def supports_subscription_channel(self, channel: str) -> bool:\n        raise NotImplementedError()\n\n    def is_admin(self):\n        user_id = self.id\n        return user_id in get_app().admin_ids\n\n    def is_feature_active(self, feature_id: str) -> bool:\n        return models.Feature.is_user_participating(self.get_db_user(), feature_id)\n\n    def get_data(self) -> Optional[Dict]:\n        user = self.get_db_user()\n        if user is None:\n            return None\n\n        data = user.data\n\n        if data is None:\n            return None\n\n        try:\n            return json.loads(data)\n        except json.JSONDecodeError:\n            return None\n\n    def set_data(self, data: Optional[Dict]):\n        user = self.get_db_user()\n        if user is None:\n            return\n\n        if data is None:\n            user.data = None\n        else:\n            user.data = json.dumps(data)\n\n    def get_message_handler(self) -> messages.MessageHandler:\n        raise NotImplementedError()\n\n    def send_message(self, message: 'messages.Message') -> 'messages.MessageSendResult':\n        result = self.get_message_handler().send_message(self, message)\n\n        app = get_app()\n        if app.config.get('VERBOSE'):\n            print('Sending message to user {} got result {}'.format(self.id, result),\n                  flush=True)\n\n        return result\n\n    def send_message_or_remove(self, channel: str, message: 'messages.Message') -> bool:\n        message_result = self.send_message(message)\n\n        if message_result == messages.MessageSendResult.UNSUPPORTED:\n            # Messages unsupported? Disable subscription then\n            print('User {} does not support messages, removing from subscription list'.format(self.id), flush=True)\n\n            # FIXME: For unsupported messages, we should mark the user unreachable for this specific channel instead\n            self.mark_unreachable()\n            return True\n        if message_result == messages.MessageSendResult.UNREACHABLE:\n            # Unreachable = Facebook is blocking us from sending, stop trying to send in the future\n            print('User {} is unreachable, removing from subscription list'.format(self.id), flush=True)\n\n            self.mark_unreachable()\n            return True\n        if message_result == messages.MessageSendResult.GONE:\n            # Gone = User no longer exists, delete from database\n            print('User {} is gone, removing from database'.format(self.id), flush=True)\n\n            self.remove_from_db()\n            return True\n\n        return False\n\n    def __repr__(self):\n        user_id = self.id\n        return 'User: {}'.format(user_id)\n\n\nclass UnifiedUserManager(UserManager):\n    def __init__(self):\n        self._managers: Dict[str, UserManager] = dict()\n\n    def register_manager(self, manager: UserManager):\n        if manager.get_identifier() in self._managers:\n            raise ValueError('Multiple managers registered for one provider')\n        if isinstance(manager, UnifiedUserManager):\n            raise ValueError('Cannot register the unified user manager')\n\n        self._managers[manager.get_identifier()] = manager\n\n    def get_user(self, user: 'Union[UserId, models.AppUser]', **kwargs) -> 'User':\n        if user.provider not in self._managers:\n            raise ValueError('Unknown user provider')\n\n        return self._managers[user.provider].get_user(user, **kwargs)\n\n    def get_administrators(self):\n        return functools.reduce(list.__add__, [manager.get_administrators() for manager in self._managers.values()])\n\n    def initialise(self):\n        for manager in self._managers.values():\n            manager.initialise()\n\n    def get_identifier(self):\n        return None\n"
  },
  {
    "path": "komidabot/util.py",
    "content": "import traceback\nfrom functools import wraps\nfrom typing import List, Tuple, TypeVar\n\nimport komidabot.localisation as localisation\nimport komidabot.translation as translation\n\n\ndef check_exceptions(fallback=None):\n    def decorator(func):\n        @wraps(func)\n        def decorated_func(*args, **kwargs):\n            try:\n                return func(*args, **kwargs)\n            except Exception as e:\n                print('Exception raised while calling {}: {}'.format(func.__name__, e))\n                traceback.print_tb(e.__traceback__)\n\n                return fallback\n\n        return decorated_func\n\n    return decorator\n\n\nT = TypeVar('T')\n\n\ndef get_list_diff(old_list: List[T], new_list: List[T]) -> Tuple[List[T], List[T], List[T]]:\n    \"\"\"\n    Computes the difference between two lists.\n    :param old_list: The old list.\n    :param new_list: The new list.\n    :return: A 3-tuple containing the following lists in order: items still present, items added, items removed\n    \"\"\"\n\n    unchanged = [item for item in old_list if item in new_list]\n    added = [item for item in new_list if item not in unchanged]\n    removed = [item for item in old_list if item not in unchanged]\n\n    assert len(unchanged) + len(removed) == len(old_list), 'List difference incorrect? {} + {} != {}'.format(unchanged,\n                                                                                                             removed,\n                                                                                                             old_list)\n    assert len(unchanged) + len(added) == len(new_list), 'List difference incorrect? {} + {} != {}'.format(unchanged,\n                                                                                                           added,\n                                                                                                           new_list)\n\n    return unchanged, added, removed\n\n\ndef date_to_string(locale: str, date):\n    if locale == translation.LANGUAGE_ENGLISH:\n        day_number = date.day\n        if day_number == 1 or day_number == 21 or day_number == 31:\n            suffix = 'st'\n        elif day_number == 2 or day_number == 22:\n            suffix = 'nd'\n        elif day_number == 3 or day_number == 23:\n            suffix = 'rd'\n        else:\n            suffix = 'th'\n\n        return '{weekday} {day}{suffix} of {month}'.format(day=date.day, suffix=suffix,\n                                                           month=localisation.MONTHS[date.month - 1](locale),\n                                                           weekday=localisation.DAYS[date.weekday()](locale))\n    elif locale == translation.LANGUAGE_DUTCH:\n        return '{weekday} {day} {month}'.format(day=date.day, month=localisation.MONTHS[date.month - 1](locale),\n                                                weekday=localisation.DAYS[date.weekday()](locale))\n    else:\n        return str(date)\n\n\ndef expected(name, value, *types):\n    types_str = ' or '.join(type_obj.__name__ for type_obj in types)\n    return ValueError('{} expected {} got {}'.format(name, types_str, type(value).__name__))\n\n\ndef expected_or_none(value, *types):\n    return expected(value, *types, type(None))\n"
  },
  {
    "path": "komidabot/web/constants.py",
    "content": "PROVIDER_ID = 'web'\n"
  },
  {
    "path": "komidabot/web/messages.py",
    "content": "import copy\nimport json\n\nfrom pywebpush import webpush, WebPushException\n\nimport komidabot.localisation as localisation\nimport komidabot.menu\nimport komidabot.messages as messages\nimport komidabot.translation as translation\nimport komidabot.users as users\nimport komidabot.util as util\nimport komidabot.web.constants as web_constants\nfrom komidabot.app import get_app\nfrom komidabot.models import CourseType, Menu\n\nVAPID_CLAIMS = {\n    'sub': 'mailto:komidabot@gmail.com'\n}\n\n\nclass MessageHandler(messages.MessageHandler):\n    def send_message(self, user: users.User, message: messages.Message) -> messages.MessageSendResult:\n        if user.id.provider != web_constants.PROVIDER_ID:\n            raise ValueError('User id is not for {}'.format(web_constants.PROVIDER_ID))\n\n        if isinstance(message, messages.TextMessage):\n            return self._send_text_message(user, message)\n        elif isinstance(message, messages.MenuMessage):\n            return self._send_menu_message(user, message)\n        elif isinstance(message, messages.SubscriptionMenuMessage):\n            return self._send_subscription_menu_message(user, message)\n        else:\n            return messages.MessageSendResult.UNSUPPORTED\n\n    @staticmethod\n    def _send_notification(subscription_information, data) -> messages.MessageSendResult:\n        app = get_app()\n\n        try:\n            response = webpush(\n                subscription_info=subscription_information,\n                data=json.dumps(data),\n                vapid_private_key=app.config['VAPID_PRIVATE_KEY'],\n                vapid_claims=copy.deepcopy(VAPID_CLAIMS)\n            )\n\n            if app.config.get('VERBOSE'):\n                print('Received {} for push {}'.format(response.status_code, subscription_information['endpoint']),\n                      flush=True)\n                print(response.content, flush=True)\n\n            return messages.MessageSendResult.SUCCESS\n        except WebPushException as e:\n            response = e.response\n\n            if app.config.get('VERBOSE'):\n                print('Received {} for push {}'.format(response.status_code, subscription_information['endpoint']),\n                      flush=True)\n                print(response.content, flush=True)\n\n            if 500 <= response.status_code < 600:\n                return messages.MessageSendResult.EXTERNAL_ERROR\n\n            if response.status_code == 429:  # Too many requests, rate limited\n                pass  # TODO: Handle rate-limiting\n            if response.status_code == 400:  # Invalid request\n                return messages.MessageSendResult.ERROR\n            if response.status_code == 404:  # Subscription not found\n                return messages.MessageSendResult.GONE\n            if response.status_code == 410:  # Subscription has been removed\n                return messages.MessageSendResult.GONE\n            if response.status_code == 413:  # Payload too large\n                return messages.MessageSendResult.ERROR\n\n            return messages.MessageSendResult.ERROR\n\n    @staticmethod\n    def _send_text_message(user: users.User, message: messages.TextMessage) -> messages.MessageSendResult:\n        subscription_information = copy.deepcopy(user.get_data())\n        subscription_information['endpoint'] = user.get_internal_id()\n\n        data = {\n            'notification': {\n                # 'lang': 'NL',\n                'badge': 'https://komidabot.xyz/assets/icons/notification-badge-android-72x72.png',\n                'title': 'Komidabot message',\n                'body': message.text,\n                'vibrate': [],\n                'renotify': False,\n                'requireInteraction': False,\n                'actions': [],\n                'silent': False,\n            }\n        }\n\n        return MessageHandler._send_notification(subscription_information, data)\n\n    @staticmethod\n    def _send_menu_message(user: users.User, message: messages.MenuMessage) -> messages.MessageSendResult:\n        locale = user.get_locale() or translation.LANGUAGE_DUTCH\n        menu = message.menu\n\n        date_str = util.date_to_string(locale, menu.menu_day)\n\n        title = localisation.REPLY_MENU_START(locale).format(campus=menu.campus.name, date=date_str)\n        text = komidabot.menu.get_short_menu_text(menu, message.translator, locale,\n                                                  CourseType.DAILY, CourseType.PASTA, CourseType.GRILL)\n\n        if text is None or text == '':\n            return messages.MessageSendResult.ERROR\n\n        subscription_information = copy.deepcopy(user.get_data())\n        subscription_information['endpoint'] = user.get_internal_id()\n\n        data = {\n            'notification': {\n                'lang': locale,\n                'badge': 'https://komidabot.xyz/assets/icons/notification-badge-android-72x72.png',\n                'title': title,\n                'body': text,\n                'renotify': False,\n                'requireInteraction': False,\n                'actions': [],\n                'silent': True,\n            }\n        }\n\n        return MessageHandler._send_notification(subscription_information, data)\n\n    @staticmethod\n    def _send_subscription_menu_message(user: users.User,\n                                        message: messages.SubscriptionMenuMessage) -> messages.MessageSendResult:\n        campus = user.get_campus_for_day(message.date)\n        if campus is None:\n            # If no campus for selected day, just success it\n            return messages.MessageSendResult.SUCCESS\n\n        locale = user.get_locale() or translation.LANGUAGE_DUTCH\n\n        data = message.get_prepared(campus, locale, user.get_provider_name())\n\n        if data is None:\n            menu = Menu.get_menu(campus, message.date)\n\n            date_str = util.date_to_string(locale, menu.menu_day)\n\n            title = localisation.REPLY_MENU_START(locale).format(campus=campus.name, date=date_str)\n            text = komidabot.menu.get_short_menu_text(menu, message.translator, locale,\n                                                      CourseType.DAILY, CourseType.PASTA, CourseType.GRILL)\n\n            if text is None or text == '':\n                return messages.MessageSendResult.ERROR\n\n            data = {\n                'notification': {\n                    'lang': locale,\n                    'badge': 'https://komidabot.xyz/assets/icons/notification-badge-android-72x72.png',\n                    'title': title,\n                    'body': text,\n                    'renotify': False,\n                    'requireInteraction': False,\n                    'actions': [],\n                    'silent': True,\n                }\n            }\n\n            message.set_prepared(campus, locale, user.get_provider_name(), data)\n\n        subscription_information = copy.deepcopy(user.get_data())\n        subscription_information['endpoint'] = user.get_internal_id()\n\n        return MessageHandler._send_notification(subscription_information, copy.deepcopy(data))\n"
  },
  {
    "path": "komidabot/web/users.py",
    "content": "from typing import Dict, Optional, TypedDict, Union\n\nimport komidabot.messages as messages\nimport komidabot.models as models\nimport komidabot.users as users\nimport komidabot.web.constants as web_constants\nfrom komidabot.web.messages import MessageHandler as WebMessageHandler\n\n__all__ = ['User', 'UserData', 'UserManager']\n\n\nclass UserManager(users.UserManager):\n    def __init__(self):\n        self.message_handler = WebMessageHandler()\n\n    def get_user(self, user: 'Union[users.UserId, models.AppUser]', **kwargs) -> 'User':\n        if isinstance(user, models.AppUser):\n            return User(self, user.internal_id)\n\n        if user.provider != web_constants.PROVIDER_ID:\n            raise ValueError('User id is not for {}'.format(web_constants.PROVIDER_ID))\n\n        # TODO: This probably could use more checks or something\n        #       For example: check if there is a subscription\n        return User(self, user.id)\n\n    def initialise(self):\n        pass\n\n    def get_identifier(self):\n        return web_constants.PROVIDER_ID\n\n\nclass User(users.User):\n    def __init__(self, manager: UserManager, id_str: str):\n        self._manager = manager\n        self._id = id_str\n\n    def get_provider_name(self) -> 'str':\n        return web_constants.PROVIDER_ID\n\n    def get_internal_id(self) -> 'str':\n        return self._id\n\n    def supports_subscription_channel(self, channel: str) -> bool:\n        return channel in []\n\n    def get_manager(self) -> UserManager:\n        return self._manager\n\n    def get_message_handler(self) -> messages.MessageHandler:\n        return self._manager.message_handler\n\n    def get_data(self) -> 'Optional[UserData]':\n        return super().get_data()\n\n    def set_data(self, data: 'Optional[UserData]'):\n        return super().set_data(data)\n\n\nclass UserData(TypedDict):\n    keys: Dict[str, str]\n"
  },
  {
    "path": "learning-data/.gitignore",
    "content": "*.yml\n*.json\n"
  },
  {
    "path": "learning-data/.gitkeep",
    "content": ""
  },
  {
    "path": "manage.py",
    "content": "import datetime\nimport glob\nimport json\nimport os\nimport signal\nimport sys\nimport traceback\nimport unittest\nfrom typing import Optional\n\nimport click\nfrom colour_runner.runner import ColourTextTestRunner\nfrom flask import current_app\nfrom flask.cli import FlaskGroup\n\nimport komidabot.models as models\nfrom app import create_app\nfrom komidabot.models_training import LearningDatapoint\n\ncli = FlaskGroup(create_app=create_app)\n\n\n@cli.command('recreate_db')\ndef recreate_db():\n    models.recreate_db()\n\n\n@cli.command('seed_db')\ndef seed_db():\n    models.create_standard_values()\n    models.import_dump(current_app.config['DUMP_FILE'])\n\n\n@cli.command('run_subscription')\ndef run_subscription():\n    raise NotImplementedError()\n\n\n@cli.command('update_menus')\ndef update_menus():\n    raise NotImplementedError()\n\n\n@cli.command('cleanup')\ndef cleanup():\n    raise NotImplementedError()\n\n\n@cli.command('synchronize_menus')\ndef synchronize_menus():\n    raise NotImplementedError()\n\n\n@cli.command('upload_learning_data')\ndef upload_learning_data():\n    import komidabot.external_menu as external_menu\n    from extensions import db\n    from komidabot.rate_limit import Limiter\n\n    limiter = Limiter(10)\n    files = glob.glob(os.path.join(os.path.dirname(__file__), 'learning-data', '*.json'))\n\n    for file in sorted(files):\n        limiter()\n        print(os.path.basename(file))\n\n        try:\n            with open(file, 'r') as f:\n                data = json.load(f)\n        except KeyboardInterrupt:\n            raise\n        except json.JSONDecodeError:\n            print('Could not decode:', file)\n            continue\n\n        campus = models.Campus.get_by_short_name(data['restaurant'])\n        date = datetime.date.fromisoformat(data['date'])\n\n        try:\n            data_raw = external_menu.fetch_raw(campus, date)\n            data_parsed = external_menu.parse_fetched(data_raw)\n            data_processed = external_menu.process_parsed(data_parsed)\n        except KeyboardInterrupt:\n            raise\n        except Exception as e:\n            print('Failure parsing external menu for:', file)\n\n            traceback.print_tb(e.__traceback__)\n            print(e, flush=True, file=sys.stderr)\n\n            continue\n\n        reference_menu: list = data['menu']\n        processed_menu: list = data_processed['menu'] if data_processed is not None else []\n\n        matched = []\n\n        for reference_item in reference_menu:\n            i = 0\n\n            for processed_item in processed_menu:\n                if processed_item['name']['nl'].lower() == reference_item['course_name'].lower():\n                    matched.append((reference_item, processed_item))\n                    break\n\n                i = i + 1\n            else:\n                print('Could not match reference item', reference_item['course_name'].lower())\n                continue\n\n            processed_menu.pop(i)\n\n        for processed_item in processed_menu:\n            print('Could not match processed item', processed_item['name']['nl'].lower())\n\n        try:\n            for reference_item, processed_item in matched:\n                LearningDatapoint.create(campus, date, reference_item['screenshot'], processed_item)\n        except KeyboardInterrupt:\n            raise\n        except Exception:\n            print('Failure adding to database for', file)\n            continue\n\n    db.session.commit()\n\n\n@cli.command('test', with_appcontext=False)\n@click.option('--case')\ndef test(case: Optional[str]):\n    \"\"\"Runs the tests without code coverage\"\"\"\n    if case:\n        tests = unittest.TestLoader().loadTestsFromName('tests.' + case)\n    else:\n        tests = unittest.TestLoader().discover('tests', pattern='test_*.py')\n    result = ColourTextTestRunner(verbosity=2).run(tests)\n    if result.wasSuccessful():\n        return 0\n    # This makes Flask return an exit code of 1, otherwise it defaults to 0 even if returning 0\n    raise click.exceptions.Exit(1)\n\n\ndef handler(signum: int, _):\n    if signum == signal.SIGTERM:\n        print('Performing shutdown')\n        os.kill(os.getpid(), signal.SIGINT)\n\n\nif __name__ == '__main__':\n    signal.signal(signal.SIGTERM, handler)\n    cli()\n"
  },
  {
    "path": "manual_menu_scraper.py",
    "content": "import datetime\nimport sys\n\nimport komidabot.external_menu as external_menu\nfrom komidabot.debug.state import DebuggableException\nfrom komidabot.models import Campus, course_icons_matrix, CourseType, CourseSubType\n\nif __name__ == '__main__':\n    # Setup\n    campuses = {\n        'cst': Campus.create('Stadscampus', 'cst', [], 1, add_to_db=False),\n        'cde': Campus.create('Campus Drie Eiken', 'cde', [], 2, add_to_db=False),\n        'cmi': Campus.create('Campus Middelheim', 'cmi', [], 3, add_to_db=False),\n        'cgb': Campus.create('Campus Groenenborger', 'cgb', [], 4, add_to_db=False),\n        'cmu': Campus.create('Campus Mutsaard', 'cmu', [], 5, add_to_db=False),\n        'hzs': Campus.create('Hogere Zeevaartschool', 'hzs', [], 6, add_to_db=False),\n    }\n    campuses_reverse = {campus.external_id: campus for campus in campuses.values()}\n\n\n    def get_by_external_id(campus_id: int):\n        return campuses_reverse.get(campus_id, None)\n\n\n    def get_by_short_name(short_name: str):\n        return campuses.get(short_name, None)\n\n\n    # Replace these methods because we don't have database access\n    Campus.get_by_external_id = get_by_external_id\n    Campus.get_by_short_name = get_by_short_name\n\n    # Actual program logic\n\n    if sys.argv[1] not in campuses:\n        raise ValueError('Unknown campus')\n    campus = campuses[sys.argv[1]]\n\n    if len(sys.argv) > 2:\n        dates = [datetime.datetime.strptime(arg, '%Y-%m-%d').date() for arg in sys.argv[2:]]\n    else:\n        dates = [datetime.datetime.today().date()]\n\n    for date in dates:\n        try:\n            data_raw = external_menu.fetch_raw(campus, date)\n\n            data_parsed = external_menu.parse_fetched(data_raw)\n\n            data_processed = external_menu.process_parsed(data_parsed)\n\n            print('{} @ {}'.format(data_processed['campus'], data_processed['date']), flush=True)\n\n            for item in data_processed['menu']:\n                icon = course_icons_matrix[CourseType[item['course_type']]][CourseSubType[item['course_sub_type']]]\n\n                print('{external_id} {type} {sub_type} {attributes} {allergens} {icon} {text} ({price1} / {price2})'\n                      .format(icon=icon,\n                              text=item['name']['nl'],\n                              price1=item['price_students'],\n                              price2=item['price_staff'],\n                              type=item['course_type'],\n                              sub_type=item['course_sub_type'],\n                              attributes=item['course_attributes'],\n                              allergens=item['course_allergens'],\n                              external_id=item['external_id'])\n                      )\n        except DebuggableException as e:\n            print(e.get_trace())\n"
  },
  {
    "path": "migrations/README",
    "content": "Generic single-database configuration."
  },
  {
    "path": "migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nfrom flask import current_app\nconfig.set_main_option(\n    'sqlalchemy.url', current_app.config.get(\n        'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=target_metadata, literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = engine_from_config(\n        config.get_section(config.config_ini_section),\n        prefix='sqlalchemy.',\n        poolclass=pool.NullPool,\n    )\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "migrations/versions/1a2e04608ee9_.py",
    "content": "\"\"\"Add web_subscriptions and provider column to registered_user table\n\nRevision ID: 1a2e04608ee9\nRevises: d225cbda8c77\nCreate Date: 2020-10-25 18:55:31.881046\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '1a2e04608ee9'\ndown_revision = 'd225cbda8c77'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('registered_user', sa.Column('provider', sa.String(length=16), nullable=False))\n    op.add_column('registered_user', sa.Column('web_subscriptions', sa.String(), server_default='[]', nullable=False))\n\n\ndef downgrade():\n    op.drop_column('registered_user', 'web_subscriptions')\n    op.drop_column('registered_user', 'provider')\n"
  },
  {
    "path": "migrations/versions/1dafd2bf730a_.py",
    "content": "\"\"\"Add course allergens column to menu item table\n\nRevision ID: 1dafd2bf730a\nRevises: aa31c90dc353\nCreate Date: 2020-10-28 15:32:49.787976\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '1dafd2bf730a'\ndown_revision = 'aa31c90dc353'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('menu_item', sa.Column('course_allergens', sa.Text(), server_default='[]', nullable=False))\n\n\ndef downgrade():\n    op.drop_column('menu_item', 'course_allergens')\n"
  },
  {
    "path": "migrations/versions/276ad61a41a5_.py",
    "content": "\"\"\"Change food type in menu items to course type and sub type\n\nRevision ID: 276ad61a41a5\nRevises: ddf5bd871988\nCreate Date: 2020-03-10 12:23:22.996161\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\nfrom sqlalchemy.dialects import postgresql as pg\n\n# revision identifiers, used by Alembic.\nrevision = '276ad61a41a5'\ndown_revision = 'ddf5bd871988'\nbranch_labels = None\ndepends_on = None\n\ncourse_type = pg.ENUM('SOUP', 'DAILY', 'PASTA', 'GRILL', 'SALAD', 'SUB', name='coursetype')\ncourse_sub_type = pg.ENUM('NORMAL', 'VEGAN', name='coursesubtype')\n\n\ndef upgrade():\n    course_type.create(op.get_bind())\n    course_sub_type.create(op.get_bind())\n\n    op.add_column('menu_item', sa.Column('course_type', course_type, nullable=True))\n    op.add_column('menu_item', sa.Column('course_sub_type', course_sub_type, nullable=True))\n    op.add_column('menu_item', sa.Column('course_attributes', sa.Text(), nullable=False,\n                                         default='[]', server_default='[]'))\n\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'SOUP', course_sub_type = 'NORMAL'\n    WHERE food_type = 'SOUP'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'DAILY', course_sub_type = 'NORMAL'\n    WHERE food_type = 'MEAT'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'DAILY', course_sub_type = 'VEGAN'\n    WHERE food_type = 'VEGAN'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'GRILL', course_sub_type = 'NORMAL'\n    WHERE food_type = 'GRILL'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'PASTA', course_sub_type = 'NORMAL'\n    WHERE food_type = 'PASTA_MEAT'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'PASTA', course_sub_type = 'VEGAN'\n    WHERE food_type = 'PASTA_VEGAN'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'SALAD', course_sub_type = 'NORMAL'\n    WHERE food_type = 'SALAD'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE menu_item\n    SET course_type = 'SUB', course_sub_type = 'NORMAL'\n    WHERE food_type = 'SUB'\n    \"\"\")\n\n    op.alter_column('menu_item', 'course_type', nullable=False)\n    op.alter_column('menu_item', 'course_sub_type', nullable=False)\n\n\ndef downgrade():\n    op.drop_column('menu_item', 'course_attributes')\n    op.drop_column('menu_item', 'course_sub_type')\n    op.drop_column('menu_item', 'course_type')\n\n    course_sub_type.drop(op.get_bind())\n    course_type.drop(op.get_bind())\n"
  },
  {
    "path": "migrations/versions/2887dcc37788_.py",
    "content": "\"\"\"Change registered_users table to have an internal id, rather than having a primary key based on 2 columns\n\nRevision ID: 2887dcc37788\nRevises: eda0c928c279\nCreate Date: 2020-11-02 22:43:08.274496\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '2887dcc37788'\ndown_revision = 'eda0c928c279'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # Primary key shuffling\n    op.rename_table('registered_user', 'registered_users')\n    op.alter_column('registered_users', 'id', new_column_name='subject')\n    id_seq = sa.Sequence('registered_users_id_seq')\n    op.execute(sa.schema.CreateSequence(id_seq))\n    op.add_column('registered_users', sa.Column('id', sa.Integer(), nullable=False, server_default=id_seq.next_value()))\n\n    op.drop_constraint('learning_datapoint_submission_user_id_fkey', 'learning_datapoint_submission',\n                       type_='foreignkey')\n    op.drop_constraint('registered_user_pkey', 'registered_users', type_='primary')\n    op.create_unique_constraint('registered_users_provider_subject_key', 'registered_users', ['provider', 'subject'])\n\n    # Foreign keys also need updating\n    op.alter_column('learning_datapoint_submission', 'user_id', new_column_name='user_subject')\n    op.add_column('learning_datapoint_submission',\n                  sa.Column('user_id', sa.Integer(), autoincrement=False))\n    op.execute(\"\"\"\n    UPDATE learning_datapoint_submission\n    SET user_id = users.id\n    FROM (SELECT id, subject, provider FROM registered_users) AS users\n    WHERE learning_datapoint_submission.user_subject = users.subject\n        AND learning_datapoint_submission.user_provider = users.provider\n    \"\"\")\n    op.alter_column('learning_datapoint_submission', 'user_id', nullable=False)\n    op.drop_constraint('learning_datapoint_submission_pkey', 'learning_datapoint_submission', type_='primary')\n    op.create_primary_key('learning_datapoint_submission_pkey', 'learning_datapoint_submission',\n                          ['user_id', 'datapoint_id'])\n    op.drop_column('learning_datapoint_submission', 'user_provider')\n    op.drop_column('learning_datapoint_submission', 'user_subject')\n\n    op.create_primary_key('registered_users_pkey', 'registered_users', ['id'])\n    op.create_foreign_key('learning_datapoint_submission_user_id_fkey', 'learning_datapoint_submission',\n                          'registered_users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')\n\n    # Replace enabled with more informative version having dates\n    op.add_column('registered_users', sa.Column('activated_on', sa.DateTime(), nullable=True))\n    op.add_column('registered_users', sa.Column('registered_on', sa.DateTime(), server_default=sa.text('now()'),\n                                                nullable=False))\n    op.execute(\"UPDATE registered_users SET activated_on = NOW() WHERE enabled = TRUE\")\n    op.drop_column('registered_users', 'enabled')\n\n    # Add new tables for roles\n    op.create_table(\n        'roles',\n        sa.Column('id', sa.Integer(), nullable=False),\n        sa.Column('name', sa.String(length=64), nullable=False),\n        sa.PrimaryKeyConstraint('id'),\n        sa.UniqueConstraint('name')\n    )\n    op.create_table(\n        'user_roles',\n        sa.Column('user_id', sa.Integer(), nullable=False),\n        sa.Column('role_id', sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'),\n        sa.ForeignKeyConstraint(['user_id'], ['registered_users.id'], ondelete='CASCADE'),\n        sa.PrimaryKeyConstraint('user_id', 'role_id')\n    )\n\n\ndef downgrade():\n    # Remove roles tables\n    op.drop_table('user_roles')\n    op.drop_table('roles')\n\n    # Bring back enabled column\n    op.add_column('registered_users', sa.Column('enabled', sa.BOOLEAN(), server_default=sa.text('false'),\n                                                autoincrement=False, nullable=False))\n    op.execute(\"UPDATE registered_users SET enabled = TRUE WHERE activated_on IS NOT NULL\")\n    op.drop_column('registered_users', 'registered_on')\n    op.drop_column('registered_users', 'activated_on')\n\n    # Undo foreign keys updating after primary key shuffling\n    op.drop_constraint('learning_datapoint_submission_user_id_fkey', 'learning_datapoint_submission',\n                       type_='foreignkey')\n    op.drop_constraint('registered_users_pkey', 'registered_users', type_='primary')\n\n    op.add_column('learning_datapoint_submission',\n                  sa.Column('user_subject', sa.String(), autoincrement=False))\n    op.add_column('learning_datapoint_submission',\n                  sa.Column('user_provider', sa.String(16), autoincrement=False))\n    op.drop_constraint('learning_datapoint_submission_pkey', 'learning_datapoint_submission', type_='primary')\n    op.create_primary_key('learning_datapoint_submission_pkey', 'learning_datapoint_submission',\n                          ['user_subject', 'user_provider', 'datapoint_id'])\n    op.execute(\"\"\"\n    UPDATE learning_datapoint_submission\n    SET user_subject = users.subject, user_provider = users.provider\n    FROM (SELECT id, subject, provider FROM registered_users) AS users\n    WHERE learning_datapoint_submission.user_id = users.id\n    \"\"\")\n    op.alter_column('learning_datapoint_submission', 'user_subject', nullable=False)\n    op.alter_column('learning_datapoint_submission', 'user_provider', nullable=False)\n    op.drop_column('learning_datapoint_submission', 'user_id')\n    op.alter_column('learning_datapoint_submission', 'user_subject', new_column_name='user_id')\n\n    op.drop_constraint('registered_users_provider_subject_key', 'registered_users', type_='unique')\n    op.create_primary_key('registered_user_pkey', 'registered_users', ['subject', 'provider'])\n    op.create_foreign_key('learning_datapoint_submission_user_id_fkey', 'learning_datapoint_submission',\n                          'registered_users', ['user_id', 'user_provider'], ['subject', 'provider'],\n                          onupdate='CASCADE', ondelete='CASCADE')\n\n    # Undo primary key shuffling\n    op.drop_column('registered_users', 'id')\n    op.execute(sa.schema.DropSequence(sa.Sequence('registered_users_id_seq')))\n    op.alter_column('registered_users', 'subject', new_column_name='id')\n    op.rename_table('registered_users', 'registered_user')\n"
  },
  {
    "path": "migrations/versions/3806b46f7f00_.py",
    "content": "\"\"\"Modify model for external API\n\nRevision ID: 3806b46f7f00\nRevises: 4fafafd2400f\nCreate Date: 2019-11-03 23:26:02.357848\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = '3806b46f7f00'\ndown_revision = '4fafafd2400f'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('campus', sa.Column('external_id', sa.Integer(), nullable=True))\n    op.alter_column('campus', 'page_url',\n                    existing_type=sa.TEXT(),\n                    server_default=None,\n                    nullable=True)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('campus', 'page_url',\n                    existing_type=sa.TEXT(),\n                    server_default='',\n                    nullable=False)\n    op.drop_column('campus', 'external_id')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/4fafafd2400f_.py",
    "content": "\"\"\"Add new FoodType enum values\n\nRevision ID: 4fafafd2400f\nRevises: 7751a57b029e\nCreate Date: 2019-10-28 19:54:52.943891\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '4fafafd2400f'\ndown_revision = '7751a57b029e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    COMMIT\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE foodtype ADD VALUE IF NOT EXISTS 'SALAD' AFTER 'PASTA_VEGAN'\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE foodtype ADD VALUE IF NOT EXISTS 'SUB' AFTER 'SALAD'\n    \"\"\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/528821121657_.py",
    "content": "\"\"\"Rename user_subscription to user_day_campus_preference\n\nRevision ID: 528821121657\nRevises: bd04cd56036f\nCreate Date: 2020-10-20 17:47:05.866470\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '528821121657'\ndown_revision = 'bd04cd56036f'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.rename_table('user_subscription', 'user_day_campus_preference')\n\n\ndef downgrade():\n    op.rename_table('user_day_campus_preference', 'user_subscription')\n"
  },
  {
    "path": "migrations/versions/55696107a6b9_.py",
    "content": "\"\"\"Add columns for feature participation\n\nRevision ID: 55696107a6b9\nRevises: 79e0c9de90f0\nCreate Date: 2019-10-14 13:17:49.813805\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = '55696107a6b9'\ndown_revision = '79e0c9de90f0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('feature',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('string_id', sa.String(length=256), nullable=False, unique=True),\n                    sa.Column('description', sa.Text(), nullable=True),\n                    sa.Column('globally_available', sa.Boolean(), default=False, nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('feature_participation',\n                    sa.Column('user_id', sa.Integer(), nullable=False),\n                    sa.Column('feature_id', sa.Integer(), nullable=False),\n                    sa.ForeignKeyConstraint(['feature_id'], ['feature.id'], onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.PrimaryKeyConstraint('user_id', 'feature_id')\n                    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('feature_participation')\n    op.drop_table('feature')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/5cd86de4dffe_.py",
    "content": "\"\"\"Drop onboarding_done as this will not be necessary anymore, and add an enabled field.\n\nRevision ID: 5cd86de4dffe\nRevises: 93b9de63cd7b\nCreate Date: 2020-02-25 10:59:52.562751\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '5cd86de4dffe'\ndown_revision = '93b9de63cd7b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('app_user', sa.Column('enabled', sa.Boolean(), nullable=False, default=True,\n                                        server_default=sa.sql.expression.true()))\n    op.drop_column('app_user', 'onboarding_done')\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('app_user', sa.Column('onboarding_done', sa.BOOLEAN(), autoincrement=False, nullable=False,\n                                        default=False, server_default=sa.sql.expression.false()))\n    op.drop_column('app_user', 'enabled')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/5ee455656a96_.py",
    "content": "\"\"\"Rename table subscription -> user\n\nRevision ID: 5ee455656a96\nRevises: 85b659320f83\nCreate Date: 2019-10-14 00:49:07.272985\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '5ee455656a96'\ndown_revision = '85b659320f83'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.rename_table('subscription', 'app_user')\n\n\ndef downgrade():\n    op.rename_table('app_user', 'subscription')\n"
  },
  {
    "path": "migrations/versions/7751a57b029e_.py",
    "content": "\"\"\"Add a table to indicate restaurant closures\n\nRevision ID: 7751a57b029e\nRevises: 55696107a6b9\nCreate Date: 2019-10-28 00:19:18.033714\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = '7751a57b029e'\ndown_revision = '55696107a6b9'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('closing_days',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('campus_id', sa.Integer(), nullable=False),\n                    sa.Column('first_day', sa.Date(), nullable=False),\n                    sa.Column('last_day', sa.Date(), nullable=False),\n                    sa.Column('translatable_id', sa.Integer(), nullable=False),\n                    sa.ForeignKeyConstraint(['campus_id'], ['campus.id'], ),\n                    sa.ForeignKeyConstraint(['translatable_id'], ['translatable.id'], onupdate='CASCADE',\n                                            ondelete='RESTRICT'),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('closing_days')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/79e0c9de90f0_.py",
    "content": "\"\"\"Split app_user subscription data into a separate table to allow a more fine-grained control over subscriptions\n\nRevision ID: 79e0c9de90f0\nRevises: 5ee455656a96\nCreate Date: 2019-10-14 01:05:50.621591\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = '79e0c9de90f0'\ndown_revision = '5ee455656a96'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('user_subscription',\n                    sa.Column('user_id', sa.Integer(), nullable=False),\n                    sa.Column('day',\n                              sa.Enum('MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY',\n                                      name='day'), nullable=False),\n                    sa.Column('campus_id', sa.Integer(), nullable=False),\n                    sa.Column('active', sa.Boolean(), nullable=False),\n                    sa.ForeignKeyConstraint(['campus_id'], ['campus.id'], onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.PrimaryKeyConstraint('user_id', 'day')\n                    )\n    op.execute(\"\"\"\n    INSERT INTO user_subscription(user_id, day, campus_id, active)\n    SELECT id, 'MONDAY', campus_mon_id, active FROM app_user\n    \"\"\")\n    op.execute(\"\"\"\n    INSERT INTO user_subscription(user_id, day, campus_id, active)\n    SELECT id, 'TUESDAY', campus_tue_id, active FROM app_user\n    \"\"\")\n    op.execute(\"\"\"\n    INSERT INTO user_subscription(user_id, day, campus_id, active)\n    SELECT id, 'WEDNESDAY', campus_wed_id, active FROM app_user\n    \"\"\")\n    op.execute(\"\"\"\n    INSERT INTO user_subscription(user_id, day, campus_id, active)\n    SELECT id, 'THURSDAY', campus_thu_id, active FROM app_user\n    \"\"\")\n    op.execute(\"\"\"\n    INSERT INTO user_subscription(user_id, day, campus_id, active)\n    SELECT id, 'FRIDAY', campus_fri_id, active FROM app_user\n    \"\"\")\n    op.drop_constraint('Subscription_campus_mon_id_fkey', 'app_user', type_='foreignkey')\n    op.drop_constraint('Subscription_campus_tue_id_fkey', 'app_user', type_='foreignkey')\n    op.drop_constraint('Subscription_campus_wed_id_fkey', 'app_user', type_='foreignkey')\n    op.drop_constraint('Subscription_campus_thu_id_fkey', 'app_user', type_='foreignkey')\n    op.drop_constraint('Subscription_campus_fri_id_fkey', 'app_user', type_='foreignkey')\n    op.drop_column('app_user', 'campus_mon_id')\n    op.drop_column('app_user', 'campus_tue_id')\n    op.drop_column('app_user', 'campus_wed_id')\n    op.drop_column('app_user', 'campus_thu_id')\n    op.drop_column('app_user', 'campus_fri_id')\n    op.drop_column('app_user', 'active')\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('app_user', sa.Column('active', sa.BOOLEAN()))\n    op.add_column('app_user', sa.Column('campus_mon_id', sa.INTEGER(), autoincrement=False))\n    op.add_column('app_user', sa.Column('campus_tue_id', sa.INTEGER(), autoincrement=False))\n    op.add_column('app_user', sa.Column('campus_wed_id', sa.INTEGER(), autoincrement=False))\n    op.add_column('app_user', sa.Column('campus_thu_id', sa.INTEGER(), autoincrement=False))\n    op.add_column('app_user', sa.Column('campus_fri_id', sa.INTEGER(), autoincrement=False))\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_fri_id = (\n        SELECT campus_id FROM user_subscription\n        WHERE user_subscription.user_id = app_user.id AND day = 'FRIDAY'\n    )\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_thu_id = (\n        SELECT campus_id FROM user_subscription\n        WHERE user_subscription.user_id = app_user.id AND day = 'THURSDAY'\n    )\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_wed_id = (\n        SELECT campus_id FROM user_subscription\n        WHERE user_subscription.user_id = app_user.id AND day = 'WEDNESDAY'\n    )\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_tue_id = (\n        SELECT campus_id FROM user_subscription\n        WHERE user_subscription.user_id = app_user.id AND day = 'TUESDAY'\n    )\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_mon_id = (\n        SELECT campus_id FROM user_subscription\n        WHERE user_subscription.user_id = app_user.id AND day = 'MONDAY'\n    )\n    \"\"\")\n    # Add some fallback data in case the user was created after migration\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_mon_id = (\n        SELECT id FROM campus\n        WHERE short_name = 'cmi'\n    )\n    WHERE campus_mon_id IS NULL\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_tue_id = (\n        SELECT id FROM campus\n        WHERE short_name = 'cmi'\n    )\n    WHERE campus_tue_id IS NULL\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_wed_id = (\n        SELECT id FROM campus\n        WHERE short_name = 'cmi'\n    )\n    WHERE campus_wed_id IS NULL\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_thu_id = (\n        SELECT id FROM campus\n        WHERE short_name = 'cmi'\n    )\n    WHERE campus_thu_id IS NULL\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET campus_fri_id = (\n        SELECT id FROM campus\n        WHERE short_name = 'cmi'\n    )\n    WHERE campus_fri_id IS NULL\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET active = 'f'\n    WHERE active IS NULL\n    \"\"\")\n    op.alter_column('app_user', 'active', server_default='t', nullable=False)\n    op.alter_column('app_user', 'campus_mon_id', nullable=False)\n    op.alter_column('app_user', 'campus_tue_id', nullable=False)\n    op.alter_column('app_user', 'campus_wed_id', nullable=False)\n    op.alter_column('app_user', 'campus_thu_id', nullable=False)\n    op.alter_column('app_user', 'campus_fri_id', nullable=False)\n    op.create_foreign_key('Subscription_campus_mon_id_fkey', 'app_user', 'campus', ['campus_mon_id'], ['id'])\n    op.create_foreign_key('Subscription_campus_tue_id_fkey', 'app_user', 'campus', ['campus_tue_id'], ['id'])\n    op.create_foreign_key('Subscription_campus_wed_id_fkey', 'app_user', 'campus', ['campus_wed_id'], ['id'])\n    op.create_foreign_key('Subscription_campus_thu_id_fkey', 'app_user', 'campus', ['campus_thu_id'], ['id'])\n    op.create_foreign_key('Subscription_campus_fri_id_fkey', 'app_user', 'campus', ['campus_fri_id'], ['id'])\n    op.drop_table('user_subscription')\n\n    # Day is generated in this migration script, so we delete it here\n    op.execute(\"\"\"DROP TYPE day\"\"\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/85b659320f83_.py",
    "content": "\"\"\"Make tables be lowercase to please Postgres\n\nRevision ID: 85b659320f83\nRevises: fe4aca6853a2\nCreate Date: 2019-10-13 23:32:53.122630\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '85b659320f83'\ndown_revision = 'fe4aca6853a2'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.rename_table('Campus', 'campus')\n    op.rename_table('Translatable', 'translatable')\n    op.rename_table('Translation', 'translation')\n    op.rename_table('Menu', 'menu')\n    op.rename_table('MenuItem', 'menu_item')\n    op.rename_table('Subscription', 'subscription')\n\n\ndef downgrade():\n    op.rename_table('campus', 'Campus')\n    op.rename_table('translatable', 'Translatable')\n    op.rename_table('translation', 'Translation')\n    op.rename_table('menu', 'Menu')\n    op.rename_table('menu_item', 'MenuItem')\n    op.rename_table('subscription', 'Subscription')\n"
  },
  {
    "path": "migrations/versions/92e4e9f8ff64_.py",
    "content": "\"\"\"Remove fields relating to the old menu parsing\n\nRevision ID: 92e4e9f8ff64\nRevises: e18b14ed6b98\nCreate Date: 2019-11-27 10:57:33.180423\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = '92e4e9f8ff64'\ndown_revision = 'e18b14ed6b98'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.alter_column('campus', 'external_id',\n                    existing_type=sa.INTEGER(),\n                    nullable=False)\n    op.drop_column('campus', 'page_url')\n\n\ndef downgrade():\n    op.add_column('campus', sa.Column('page_url', sa.TEXT(), autoincrement=False, nullable=True))\n    op.alter_column('campus', 'external_id',\n                    existing_type=sa.INTEGER(),\n                    nullable=True)\n"
  },
  {
    "path": "migrations/versions/93b9de63cd7b_.py",
    "content": "\"\"\"Add field to users to indicate whether they've received an introduction to the bot yet\n\nRevision ID: 93b9de63cd7b\nRevises: 92e4e9f8ff64\nCreate Date: 2019-11-27 16:14:21.089378\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '93b9de63cd7b'\ndown_revision = '92e4e9f8ff64'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # Manual query because apparently relying on alembic doesn't work here\n    op.execute(\"\"\"\n    ALTER TABLE app_user ADD COLUMN onboarding_done BOOLEAN NOT NULL DEFAULT FALSE\n    \"\"\")\n\n\ndef downgrade():\n    op.drop_column('app_user', 'onboarding_done')\n"
  },
  {
    "path": "migrations/versions/9b9afdcf4e4e_.py",
    "content": "\"\"\"Add column for storing whether a user has been informed about our new site at https://komidabot.xyz/\n\nRevision ID: 9b9afdcf4e4e\nRevises: 276ad61a41a5\nCreate Date: 2020-09-19 20:40:11.471923\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = '9b9afdcf4e4e'\ndown_revision = '276ad61a41a5'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('app_user', sa.Column('notified_new_site', sa.Boolean(), server_default=sa.text('false'),\n                                        nullable=False))\n\n\ndef downgrade():\n    op.drop_column('app_user', 'notified_new_site')\n"
  },
  {
    "path": "migrations/versions/a223b578f7b0_.py",
    "content": "\"\"\"Initial imported database structure\n\nRevision ID: a223b578f7b0\nRevises: \nCreate Date: 2019-10-13 22:02:33.540908\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = 'a223b578f7b0'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('Campus',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('name', sa.String(length=128), nullable=False),\n                    sa.Column('short_name', sa.String(length=8), nullable=False),\n                    sa.Column('keywords', sa.Text(), nullable=False),\n                    sa.Column('active', sa.Boolean(), nullable=False),\n                    sa.Column('page_url', sa.Text(), nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('Translatable',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('original_language', sa.String(length=5), nullable=False),\n                    sa.Column('original_text', sa.String(length=256), nullable=False),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('Menu',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('campus_id', sa.Integer(), nullable=False),\n                    sa.Column('menu_day', sa.Date(), nullable=False),\n                    sa.ForeignKeyConstraint(['campus_id'], ['Campus.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('Subscription',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('facebook_id', sa.String(length=32), nullable=False),\n                    sa.Column('active', sa.Boolean(), nullable=False),\n                    sa.Column('language', sa.String(length=5), nullable=False),\n                    sa.Column('campus_mon_id', sa.Integer(), nullable=False),\n                    sa.Column('campus_tue_id', sa.Integer(), nullable=False),\n                    sa.Column('campus_wed_id', sa.Integer(), nullable=False),\n                    sa.Column('campus_thu_id', sa.Integer(), nullable=False),\n                    sa.Column('campus_fri_id', sa.Integer(), nullable=False),\n                    sa.ForeignKeyConstraint(['campus_fri_id'], ['Campus.id'], ),\n                    sa.ForeignKeyConstraint(['campus_mon_id'], ['Campus.id'], ),\n                    sa.ForeignKeyConstraint(['campus_thu_id'], ['Campus.id'], ),\n                    sa.ForeignKeyConstraint(['campus_tue_id'], ['Campus.id'], ),\n                    sa.ForeignKeyConstraint(['campus_wed_id'], ['Campus.id'], ),\n                    sa.PrimaryKeyConstraint('id'),\n                    sa.UniqueConstraint('facebook_id')\n                    )\n    op.create_table('Translation',\n                    sa.Column('translatable_id', sa.Integer(), nullable=False),\n                    sa.Column('language', sa.String(length=5), nullable=False),\n                    sa.Column('translation', sa.String(length=256), nullable=False),\n                    sa.ForeignKeyConstraint(['translatable_id'], ['Translatable.id'], onupdate='CASCADE',\n                                            ondelete='CASCADE'),\n                    sa.PrimaryKeyConstraint('translatable_id', 'language')\n                    )\n    op.create_table('MenuItem',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('menu_id', sa.Integer(), nullable=False),\n                    sa.Column('translatable_id', sa.Integer(), nullable=False),\n                    sa.Column('food_type',\n                              sa.Enum('SOUP', 'MEAT', 'VEGAN', 'GRILL', 'PASTA_MEAT', 'PASTA_VEGAN', name='foodtype'),\n                              nullable=False),\n                    sa.Column('price_students', sa.String(length=8), nullable=False),\n                    sa.Column('price_staff', sa.String(length=8), nullable=False),\n                    sa.ForeignKeyConstraint(['menu_id'], ['Menu.id'], onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.ForeignKeyConstraint(['translatable_id'], ['Translatable.id'], onupdate='CASCADE',\n                                            ondelete='RESTRICT'),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('MenuItem')\n    op.drop_table('Translation')\n    op.drop_table('Subscription')\n    op.drop_table('Menu')\n    op.drop_table('Translatable')\n    op.drop_table('Campus')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/aa31c90dc353_.py",
    "content": "\"\"\"Add dessert type to coursetype enum\n\nRevision ID: aa31c90dc353\nRevises: daf22dcadb8d\nCreate Date: 2020-10-28 02:34:17.867680\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'aa31c90dc353'\ndown_revision = 'daf22dcadb8d'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    COMMIT\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE coursetype ADD VALUE IF NOT EXISTS 'DESSERT' AFTER 'SUB'\n    \"\"\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/b384f281e755_.py",
    "content": "\"\"\"Add column to AppUser to store data that some providers may need to store.\n\nRevision ID: b384f281e755\nRevises: ee24af8d3121\nCreate Date: 2020-02-27 12:52:40.163394\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = 'b384f281e755'\ndown_revision = 'ee24af8d3121'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('app_user', sa.Column('data', sa.Text(), nullable=True))\n\n\ndef downgrade():\n    op.drop_column('app_user', 'data')\n"
  },
  {
    "path": "migrations/versions/bc1ef0083bb4_.py",
    "content": "\"\"\"Allow closing days to not have an end date\n\nRevision ID: bc1ef0083bb4\nRevises: 9b9afdcf4e4e\nCreate Date: 2020-09-22 18:29:49.798217\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'bc1ef0083bb4'\ndown_revision = '9b9afdcf4e4e'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.alter_column('closing_days', 'last_day',\n                    existing_type=sa.DATE(),\n                    nullable=True)\n\n\ndef downgrade():\n    op.alter_column('closing_days', 'last_day',\n                    existing_type=sa.DATE(),\n                    nullable=False)\n"
  },
  {
    "path": "migrations/versions/bd04cd56036f_.py",
    "content": "\"\"\"Rename VEGAN to VEGETARIAN and add a real VEGAN enum option\n\nRevision ID: bd04cd56036f\nRevises: bc1ef0083bb4\nCreate Date: 2020-10-08 01:52:43.143274\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'bd04cd56036f'\ndown_revision = 'bc1ef0083bb4'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    COMMIT\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE coursesubtype RENAME VALUE 'VEGAN' TO 'VEGETARIAN'\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE coursesubtype ADD VALUE IF NOT EXISTS 'VEGAN' AFTER 'VEGETARIAN'\n    \"\"\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/d225cbda8c77_.py",
    "content": "\"\"\"Added registered_user and app_settings table\n\nRevision ID: d225cbda8c77\nRevises: 528821121657\nCreate Date: 2020-10-24 22:29:31.746859\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'd225cbda8c77'\ndown_revision = '528821121657'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.create_table('registered_user',\n                    sa.Column('id', sa.String(), nullable=False),\n                    sa.Column('name', sa.String(), nullable=False),\n                    sa.Column('email', sa.String(), nullable=False),\n                    sa.Column('profile_picture', sa.String(), nullable=False),\n                    sa.Column('enabled', sa.Boolean(), server_default=sa.text('false'), nullable=False),\n                    sa.PrimaryKeyConstraint('id'),\n                    sa.UniqueConstraint('email')\n                    )\n\n    op.create_table('app_settings',\n                    sa.Column('name', sa.String(), nullable=False),\n                    sa.Column('value', sa.String(), server_default='null', nullable=False),\n                    sa.PrimaryKeyConstraint('name')\n                    )\n\n\ndef downgrade():\n    op.drop_table('app_settings')\n    op.drop_table('registered_user')\n"
  },
  {
    "path": "migrations/versions/daf22dcadb8d_.py",
    "content": "\"\"\"Drop food_type type\n\nRevision ID: daf22dcadb8d\nRevises: fe7bda58c5a4\nCreate Date: 2020-10-28 01:52:05.428570\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'daf22dcadb8d'\ndown_revision = 'fe7bda58c5a4'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.drop_column('menu_item', 'food_type')\n    op.execute(\"DROP TYPE foodtype\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/ddf5bd871988_.py",
    "content": "\"\"\"Add field to Translation indicating the provider of the translation. e.g. google, bing, komida, ...\nAlso truncates language fields\n\nRevision ID: ddf5bd871988\nRevises: b384f281e755\nCreate Date: 2020-03-04 14:52:00.074936\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = 'ddf5bd871988'\ndown_revision = 'b384f281e755'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('translation', sa.Column('provider', sa.String(length=16), nullable=True))\n    op.execute(\"\"\"\n    UPDATE translation\n    SET provider = 'google',\n        language = LEFT(language, 2)\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = LEFT(original_language, 2)\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = LEFT(language, 2)\n    \"\"\")\n\n\ndef downgrade():\n    op.drop_column('translation', 'provider')\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'nl_NL'\n    WHERE language = 'nl'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'nl_NL'\n    WHERE original_language = 'nl'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'nl_NL'\n    WHERE language = 'nl'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'en_GB'\n    WHERE language = 'en'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'en_GB'\n    WHERE original_language = 'en'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'en_GB'\n    WHERE language = 'en'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'hi_IN'\n    WHERE language = 'hi'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'hi_IN'\n    WHERE original_language = 'hi'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'hi_IN'\n    WHERE language = 'hi'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'de_DE'\n    WHERE language = 'de'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'de_DE'\n    WHERE original_language = 'de'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'de_DE'\n    WHERE language = 'de'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'ko_KR'\n    WHERE language = 'ko'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'ko_KR'\n    WHERE original_language = 'ko'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'ko_KR'\n    WHERE language = 'ko'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translation\n    SET language = 'es_ES'\n    WHERE language = 'es'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE translatable\n    SET original_language = 'es_ES'\n    WHERE original_language = 'es'\n    \"\"\")\n    op.execute(\"\"\"\n    UPDATE app_user\n    SET language = 'es_ES'\n    WHERE language = 'es'\n    \"\"\")\n"
  },
  {
    "path": "migrations/versions/e18b14ed6b98_.py",
    "content": "\"\"\"Change price columns to store as numerics instead of strings\n\nRevision ID: e18b14ed6b98\nRevises: 3806b46f7f00\nCreate Date: 2019-11-07 00:53:28.115755\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'e18b14ed6b98'\ndown_revision = '3806b46f7f00'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    ALTER TABLE menu_item\n        ALTER COLUMN price_students TYPE NUMERIC(4, 2) USING\n            CASE\n                WHEN price_students = ''\n                    THEN 0.0\n                ELSE substring(REPLACE(price_students, ',', '.') FROM 2)::numeric(4,2)\n            END,\n        ALTER COLUMN price_staff TYPE NUMERIC(4, 2) USING\n            CASE\n                WHEN price_staff = ''\n                    THEN 0.0\n                ELSE substring(REPLACE(price_staff, ',', '.') FROM 2)::numeric(4,2)\n            END,\n        ALTER COLUMN price_staff DROP NOT NULL\n    \"\"\")\n\n    op.execute(\"\"\"\n    UPDATE menu_item\n        SET price_staff = NULL\n        WHERE price_staff = '0.0'\n    \"\"\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/ea6e1f581a7b_.py",
    "content": "\"\"\"Add external id column to menu items\n\nRevision ID: ea6e1f581a7b\nRevises: 1a2e04608ee9\nCreate Date: 2020-10-25 21:42:34.054774\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'ea6e1f581a7b'\ndown_revision = '1a2e04608ee9'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('menu_item', sa.Column('external_id', sa.Integer(), server_default=sa.text('NULL'), nullable=True))\n    op.create_unique_constraint(None, 'menu_item', ['external_id'])\n\n\ndef downgrade():\n    op.drop_constraint(None, 'menu_item', type_='unique')\n    op.drop_column('menu_item', 'external_id')\n"
  },
  {
    "path": "migrations/versions/ecce0e669d8c_.py",
    "content": "\"\"\"Add snack type to coursetype enum\n\nRevision ID: ecce0e669d8c\nRevises: 2887dcc37788\nCreate Date: 2020-11-03 17:35:48.126589\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'ecce0e669d8c'\ndown_revision = '2887dcc37788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    COMMIT\n    \"\"\")\n    op.execute(\"\"\"\n    ALTER TYPE coursetype ADD VALUE IF NOT EXISTS 'SNACK' AFTER 'DESSERT'\n    \"\"\")\n\n\ndef downgrade():\n    raise NotImplementedError()\n"
  },
  {
    "path": "migrations/versions/eda0c928c279_.py",
    "content": "\"\"\"Add learning datapoints table for gathering data to train a classifier\n\nRevision ID: eda0c928c279\nRevises: 1dafd2bf730a\nCreate Date: 2020-10-29 02:38:04.925464\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'eda0c928c279'\ndown_revision = '1dafd2bf730a'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # This fixes a problem where the primary key didn't get updated in a previous migration\n    op.execute(\"ALTER TABLE registered_user DROP CONSTRAINT registered_user_pkey\")\n    op.execute(\"ALTER TABLE registered_user ADD PRIMARY KEY (id, provider)\")\n\n    op.create_table('learning_datapoint',\n                    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),\n                    sa.Column('campus_id', sa.Integer(), nullable=False),\n                    sa.Column('menu_day', sa.Date(), nullable=False),\n                    sa.Column('screenshot', sa.Text(), nullable=False),\n                    sa.Column('processed_data', sa.Text(), nullable=False),\n                    sa.ForeignKeyConstraint(('campus_id',), ['campus.id'], ),\n                    sa.PrimaryKeyConstraint('id')\n                    )\n    op.create_table('learning_datapoint_submission',\n                    sa.Column('user_id', sa.String(), nullable=False),\n                    sa.Column('user_provider', sa.String(length=16), nullable=False),\n                    sa.Column('datapoint_id', sa.Integer(), nullable=False),\n                    sa.Column('submission_data', sa.Text(), nullable=False),\n                    sa.ForeignKeyConstraint(('datapoint_id',), ['learning_datapoint.id'],\n                                            onupdate='CASCADE', ondelete='CASCADE'),\n                    sa.ForeignKeyConstraint(('user_id', 'user_provider'),\n                                            ['registered_user.id', 'registered_user.provider'],\n                                            onupdate='CASCADE',\n                                            ondelete='CASCADE'),\n                    sa.PrimaryKeyConstraint('user_id', 'user_provider', 'datapoint_id')\n                    )\n\n\ndef downgrade():\n    op.drop_table('learning_datapoint_submission')\n    op.drop_table('learning_datapoint')\n"
  },
  {
    "path": "migrations/versions/ee24af8d3121_.py",
    "content": "\"\"\"Remove size constraint on AppUser internal ID.\n\nRevision ID: ee24af8d3121\nRevises: 5cd86de4dffe\nCreate Date: 2020-02-27 12:43:51.806644\n\n\"\"\"\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'ee24af8d3121'\ndown_revision = '5cd86de4dffe'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.execute(\"\"\"\n    ALTER TABLE app_user\n        ALTER COLUMN internal_id TYPE VARCHAR;\n    \"\"\")\n\n\ndef downgrade():\n    op.execute(\"\"\"\n    ALTER TABLE app_user\n        ALTER COLUMN internal_id TYPE VARCHAR(32);\n    \"\"\")\n"
  },
  {
    "path": "migrations/versions/fe4aca6853a2_.py",
    "content": "\"\"\"Change subscriptions storage from facebook_id to (provider, internal_id)\n\nRevision ID: fe4aca6853a2\nRevises: a223b578f7b0\nCreate Date: 2019-10-13 22:37:16.548775\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = 'fe4aca6853a2'\ndown_revision = 'a223b578f7b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.drop_constraint('Subscription_facebook_id_key', 'Subscription', type_='unique')\n    op.add_column('Subscription', sa.Column('provider', sa.String(length=32), server_default='facebook'))\n    op.alter_column('Subscription', 'provider', server_default=None, nullable=False)  # Define default then remove it\n    op.alter_column('Subscription', 'facebook_id', new_column_name='internal_id')\n    op.create_unique_constraint('Subscription_provider_internal_id_key', 'Subscription', ['provider', 'internal_id'])\n\n\ndef downgrade():\n    op.drop_constraint('Subscription_provider_internal_id_key', 'Subscription', type_='unique')\n    op.drop_column('Subscription', 'provider')\n    op.alter_column('Subscription', 'internal_id', new_column_name='facebook_id')\n    op.create_unique_constraint('Subscription_facebook_id_key', 'Subscription', ['facebook_id'])\n"
  },
  {
    "path": "migrations/versions/fe7bda58c5a4_.py",
    "content": "\"\"\"Add data_frozen column to menu_item table\n\nRevision ID: fe7bda58c5a4\nRevises: ea6e1f581a7b\nCreate Date: 2020-10-25 23:21:51.975403\n\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = 'fe7bda58c5a4'\ndown_revision = 'ea6e1f581a7b'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    op.add_column('menu_item', sa.Column('data_frozen', sa.Boolean(), server_default=sa.text('false'), nullable=False))\n\n\ndef downgrade():\n    op.drop_column('menu_item', 'data_frozen')\n"
  },
  {
    "path": "requirements.txt",
    "content": "wheel==0.37.1\nsetuptools==65.3.0\nFlask==2.2.2\nFlask-Login==0.6.2\nFlask-Migrate==3.1.0\nFlask-Session==0.4.0\nFlask-SQLAlchemy==2.5.1\nFlask-Testing==0.8.1\ngunicorn==20.1.0\njsonschema==3.2.0\npsycopg2-binary==2.9.3\nrequests==2.28.1\npython-dateutil==2.8.2\ngoogletrans==3.0.0\ncachetools==5.2.0\nAPScheduler==3.9.1\nhttpretty==1.1.4\ncryptography==38.0.1\npywebpush==1.14.0\npy-vapid==1.8.2\nWerkzeug==2.2.2\nboto3==1.17.*\nSQLAlchemy==1.4.41\nsqlalchemy2-stubs==0.0.2a27\ncolour-runner==0.1.1\noauthlib==3.2.1\nPyYAML==6.0.1\n\nJinja2==3.0.3\nitsdangerous==2.0.1\n"
  },
  {
    "path": "schemas/DELETE_api_subscribe.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"DeleteSubscriptionMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"endpoint\": {\n      \"type\": \"string\"\n    },\n    \"channel\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\n    \"endpoint\",\n    \"channel\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/GET_api_authorized.response.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$ref\": \"api_response_strict.json\",\n  \"title\": \"AuthorizedApiResponse\",\n  \"properties\": {\n    \"roles\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"required\": [\n    \"roles\"\n  ]\n}\n"
  },
  {
    "path": "schemas/GET_api_learning.response.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$ref\": \"api_response_strict.json\",\n  \"title\": \"LearningApiResponse\",\n  \"properties\": {\n    \"data\": {\n      \"oneOf\": [\n        {\n          \"type\": \"null\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\"\n            },\n            \"screenshot\": {\n              \"type\": \"string\"\n            },\n            \"course_name\": {\n              \"type\": \"string\"\n            },\n            \"course_type\": {\n              \"type\": \"number\",\n              \"minimum\": 1,\n              \"maximum\": 8\n            },\n            \"course_sub_type\": {\n              \"type\": \"number\",\n              \"minimum\": 1,\n              \"maximum\": 3\n            },\n            \"price_students\": {\n              \"type\": \"string\"\n            },\n            \"price_staff\": {\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\n            \"id\",\n            \"screenshot\",\n            \"course_name\",\n            \"course_type\",\n            \"course_sub_type\",\n            \"price_students\",\n            \"price_staff\"\n          ]\n        }\n      ]\n    }\n  },\n  \"required\": [\n    \"data\"\n  ]\n}\n"
  },
  {
    "path": "schemas/POST_api_learning.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"LearningPostMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"id\": {\n      \"type\": \"string\"\n    },\n    \"course_name_correct\": {\n      \"type\": \"boolean\"\n    },\n    \"course_type\": {\n      \"type\": \"integer\",\n      \"oneOf\": [\n        {\n          \"minimum\": 1,\n          \"maximum\": 8\n        },\n        {\n          \"minimum\": -2,\n          \"maximum\": -1\n        }\n      ]\n    },\n    \"course_sub_type\": {\n      \"type\": \"integer\",\n      \"minimum\": 1,\n      \"maximum\": 3\n    },\n    \"price_students_correct\": {\n      \"type\": \"boolean\"\n    },\n    \"price_staff_correct\": {\n      \"type\": \"boolean\"\n    }\n  },\n  \"required\": [\n    \"id\",\n    \"course_name_correct\",\n    \"course_type\",\n    \"course_sub_type\",\n    \"price_students_correct\",\n    \"price_staff_correct\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/POST_api_login.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"LoginMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"username\": {\n      \"type\": \"string\"\n    },\n    \"password\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\n    \"username\",\n    \"password\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/POST_api_subscribe.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"AddSubscriptionMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"endpoint\": {\n      \"type\": \"string\"\n    },\n    \"keys\": {\n      \"type\": \"object\"\n    },\n    \"channel\": {\n      \"type\": \"string\"\n    },\n    \"data\": true\n  },\n  \"required\": [\n    \"endpoint\",\n    \"keys\",\n    \"channel\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/POST_api_trigger.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"LoginMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"trigger\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"menu_update\",\n        \"notification_test_error\",\n        \"notification_test_text\"\n      ]\n    }\n  },\n  \"required\": [\n    \"username\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/PUT_api_subscribe.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"ReplaceSubscriptionMessage\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"old_endpoint\": {\n      \"type\": \"string\"\n    },\n    \"endpoint\": {\n      \"type\": \"string\"\n    },\n    \"keys\": {\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [\n    \"old_endpoint\",\n    \"endpoint\",\n    \"keys\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "schemas/api_response_base.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"ApiResponse\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"status\": {\n      \"type\": \"integer\"\n    },\n    \"message\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\n    \"status\",\n    \"message\"\n  ]\n}\n"
  },
  {
    "path": "schemas/api_response_strict.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$ref\": \"api_response_base.json\",\n  \"title\": \"StrictApiResponse\",\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/base.py",
    "content": "import datetime\nfrom decimal import Decimal\nfrom functools import partial, wraps\nfrom typing import Dict, List, NamedTuple, Tuple\n\nimport httpretty\nfrom flask.cli import ScriptInfo\nfrom flask_testing import TestCase\n\nimport komidabot.models as models\nimport komidabot.users as users\nfrom app import create_app, db\nfrom komidabot.app import App\nfrom tests.utils import StubTranslator\n\nmenu_item = NamedTuple('menu_item', [('type', models.CourseType),\n                                     ('sub_type', models.CourseSubType),\n                                     ('attributes', List[models.CourseAttributes]),\n                                     ('allergens', List[models.CourseAllergens]),\n                                     ('text', str),\n                                     ('language', str),\n                                     ('price_students', Decimal),\n                                     ('price_staff', Decimal)])\n\n\ndef with_context(func):\n    @wraps(func)\n    def decorated_func(self, *args, **kwargs):\n        if getattr(with_context, 'active', False):\n            return func(self, *args, **kwargs)\n\n        if 'has_context' in kwargs:\n            has_context = kwargs.pop('has_context')\n            if has_context:\n                try:\n                    setattr(with_context, 'active', True)\n                    return func(self, *args, **kwargs)\n                finally:\n                    setattr(with_context, 'active', False)\n\n        with self.app.app_context():\n            try:\n                setattr(with_context, 'active', True)\n                return func(self, *args, **kwargs)\n            finally:\n                setattr(with_context, 'active', False)\n\n    return decorated_func\n\n\nclass BaseTestCase(TestCase):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # noinspection PyTypeChecker\n        self.app: App = None\n\n    def create_app(self):\n        script_info = ScriptInfo(create_app=partial(create_app, app_settings='config.TestingConfig'))\n        return script_info.load_app()\n\n    def setUp(self):\n        super().setUp()\n\n        self.app.translator = self.translator = StubTranslator()\n\n        with self.app.app_context():\n            db.create_all()\n            db.session.commit()\n\n    def tearDown(self):\n        with self.app.app_context():\n            db.session.remove()\n            db.drop_all()\n\n        super().tearDown()\n\n    def assertEqualCommutative(self, first, second, msg=None):\n        self.assertEqual(first, second, msg=msg)\n        self.assertEqual(second, first, msg=msg)\n\n    def assertNotEqualCommutative(self, first, second, msg=None):\n        self.assertNotEqual(first, second, msg=msg)\n        self.assertNotEqual(second, first, msg=msg)\n\n    @with_context\n    def create_translation(self, data: Dict[str, str], default_language: str) -> Tuple[models.Translatable,\n                                                                                       Dict[str, models.Translation]]:\n        if default_language not in data:\n            raise ValueError()\n\n        result = dict()\n\n        translatable, translation = models.Translatable.get_or_create(data[default_language], default_language)\n\n        result[default_language] = translation\n\n        for language, text in data.items():\n            if language == default_language:\n                continue\n\n            translation = translatable.add_translation(language, text)\n            result[language] = translation\n\n        db.session.commit()\n\n        return translatable, result\n\n    @with_context\n    def create_test_campuses(self) -> List[models.Campus]:\n        campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 0)\n        campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 0)\n        campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 0)\n        campus3.active = False\n        db.session.commit()\n\n        self.campuses = [campus1, campus2, campus3]\n\n        return self.campuses\n\n    @with_context\n    def activate_feature(self, feature_id: str, user_list: 'List[users.UserId]' = None,\n                         available=None) -> models.Feature:\n        feature = models.Feature.create(feature_id)\n\n        if user_list:\n            for user in user_list:\n                user_obj = models.AppUser.find_by_id(user.provider, user.id)\n                if user_obj is None:\n                    raise ValueError()\n                models.Feature.set_user_participating(user_obj, feature.string_id, True)\n\n        if available is not None:\n            feature.globally_available = available\n\n        db.session.commit()\n        return feature\n\n    @with_context\n    def create_menu(self, campus: models.Campus, day: datetime.date, items: 'List[menu_item]') -> models.Menu:\n        menu = models.Menu.create(campus, day)\n\n        for item in items:\n            translatable, _ = models.Translatable.get_or_create(item.text, item.language)\n            menu.add_menu_item(translatable, item.type, item.sub_type, item.attributes, item.allergens,\n                               item.price_students, item.price_staff)\n\n        db.session.commit()\n        return menu\n\n\nclass HttpCapture:\n    GET = httpretty.GET\n    PUT = httpretty.PUT\n    POST = httpretty.POST\n    DELETE = httpretty.DELETE\n    HEAD = httpretty.HEAD\n    PATCH = httpretty.PATCH\n    OPTIONS = httpretty.OPTIONS\n    CONNECT = httpretty.CONNECT\n\n    def __init__(self, allow_net_connect=False):\n        self.allow_net_connect = allow_net_connect\n\n    def __enter__(self):\n        httpretty.enable(allow_net_connect=self.allow_net_connect)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        httpretty.disable()\n        httpretty.reset()\n\n    # noinspection PyMethodMayBeStatic\n    def register_uri(self, method, uri, body, status=200):\n        httpretty.register_uri(method, uri, body, status=status)\n"
  },
  {
    "path": "tests/external_menus/.gitignore",
    "content": "*.parsed.yaml\n*.processed.yaml\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cde.parsed.expected.yaml",
    "content": "$test_case:\n  course_of_interest: 1353\n  reason: |\n    This response originally broke an assumption that only one component in a menu item can have a price attached to it.\n    However, the price is based on the sum of all components, some of which can have a fixed price, but others may\n    require \"calculating\" a staff price. If one or more requires this to be calculated this is done on the summed price.\ncampus: cde\ndate: '2019-11-25'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes:\n    - SOUP\n    - VEGGIE\n    name:\n      en: Celeriac soup\n      nl: Knolseldersoep\n  external_id: 1351\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - EGG\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - VEGGIE\n    name:\n      en: Quorn and bell pepper goulash dd\n      nl: Goulash met quorn en paprika dd\n  - allergens: [ ]\n    attributes: [ ]\n    name:\n      en: rice\n      nl: rijst\n  external_id: 1352\n  multiple_prices: true\n  price: '4.60'\n  sort_order: 1\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    name:\n      en: Chicken roulade with sun-dried tomatoes\n      nl: Kiprollade met zongedroogde tomaat\n    $test_case:\n      comment: This component has a price of 4.6 in the raw data\n  - allergens:\n    - MUSTARD\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: couscous\n      nl: couscous\n    $test_case:\n      comment: This component has a price of 0.2 in the raw data\n  - allergens:\n    - CELERY\n    attributes: [ ]\n    name:\n      en: couscous vegetables\n      nl: couscousgroenten\n  external_id: 1353\n  multiple_prices: true\n  price: '4.80'\n  $test_case:\n    comment: The price of this course is based on the sum of its components\n  sort_order: 2\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: Penne with mushrooms and a creamy cauliflower sauce\n      nl: Penne met paddenstoelen en romige bloemkoolsaus\n  external_id: 1427\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens:\n    - EGG\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: pasta\n      nl: pasta\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    attributes:\n    - PASTA\n    - VEGGIE\n    name:\n      en: African sunshine sauce\n      nl: African sunshinesaus\n  external_id: 1430\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens: [ ]\n    attributes:\n    - CHICKEN\n    - GRILL\n    name:\n      en: Grilled chicken breast\n      nl: Kipfilet op de grill\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: fries\n      nl: frieten\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - LUPINE\n    - MILK_LACTOSE\n    - MOLLUSKS\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: saladbar\n      nl: Saladbar\n  external_id: 1431\n  multiple_prices: true\n  price: '4.80'\n  sort_order: 5\n- components:\n  - allergens:\n    - SESAME\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Buddha bow hummuslicious\n      nl: Buddha bowl humuslicious\n  external_id: 1432\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 11\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cde.processed.expected.yaml",
    "content": "campus: cde\ndate: '2019-11-25'\nmenu:\n- course_allergens:\n  - CELERY\n  - MILK_LACTOSE\n  - WHEAT_GLUTEN\n  course_attributes:\n  - SOUP\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: SOUP\n  external_id: 1351\n  name:\n    en: Celeriac soup\n    nl: Knolseldersoep\n  price_staff: null\n  price_students: '0.90'\n- course_allergens:\n  - EGG\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: DAILY\n  external_id: 1352\n  name:\n    en: Quorn and bell pepper goulash dd, rice\n    nl: Goulash met quorn en paprika dd, rijst\n  price_staff: '5.70'\n  price_students: '4.60'\n- course_allergens:\n  - CELERY\n  - MILK_LACTOSE\n  - MUSTARD\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  course_sub_type: NORMAL\n  course_type: DAILY\n  external_id: 1353\n  name:\n    en: Chicken roulade with sun-dried tomatoes, couscous, couscous vegetables\n    nl: Kiprollade met zongedroogde tomaat, couscous, couscousgroenten\n  price_staff: '6.00'\n  price_students: '4.80'\n- course_allergens:\n  - WHEAT_GLUTEN\n  course_attributes:\n  - PASTA\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: PASTA\n  external_id: 1427\n  name:\n    en: Penne with mushrooms and a creamy cauliflower sauce\n    nl: Penne met paddenstoelen en romige bloemkoolsaus\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - EGG\n  - MILK_LACTOSE\n  - WHEAT_GLUTEN\n  course_attributes:\n  - PASTA\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: PASTA\n  external_id: 1430\n  name:\n    en: Pasta, African sunshine sauce\n    nl: Pasta, African sunshinesaus\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - CELERY\n  - EGG\n  - FISH\n  - LUPINE\n  - MILK_LACTOSE\n  - MOLLUSKS\n  - MUSTARD\n  - NUTS\n  - PEANUTS\n  - SESAME\n  - SHELLFISH\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  - GRILL\n  course_sub_type: NORMAL\n  course_type: GRILL\n  external_id: 1431\n  name:\n    en: Grilled chicken breast, fries, saladbar\n    nl: Kipfilet op de grill, frieten, Saladbar\n  price_staff: '6.00'\n  price_students: '4.80'\n- course_allergens:\n  - SESAME\n  course_attributes:\n  - SALAD\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: SALAD\n  external_id: 1432\n  name:\n    en: Buddha bow hummuslicious\n    nl: Buddha bowl humuslicious\n  price_staff: '4.70'\n  price_students: '3.80'\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cde.raw.json",
    "content": "{\n  \"id\": 208,\n  \"menuDate\": \"2019-11-25T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1351,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1967,\n          \"menuItemId\": 1351,\n          \"courseId\": 859,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 859,\n            \"dispNameNl\": \"Knolseldersoep\",\n            \"dispNameEn\": \"Celeriac soup\",\n            \"nameNl\": \"knolseldersoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"bouillon, ajuinblokjes, soepprei,knolselder en eventueel aardappelen samen gaar koken. - mixen. - op smaak brengen met peper en zout. - melk en room toevoegen.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 859,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 859,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 859,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 859,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 859,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1352,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1969,\n          \"menuItemId\": 1352,\n          \"courseId\": 999,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 999,\n            \"dispNameNl\": \"rijst\",\n            \"dispNameEn\": \"rice\",\n            \"nameNl\": \"rijst\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"kook de rijst gaar in licht gezouten water -\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1968,\n          \"menuItemId\": 1352,\n          \"courseId\": 1318,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1318,\n            \"dispNameNl\": \"Goulash met quorn en paprika dd\",\n            \"dispNameEn\": \"Quorn and bell pepper goulash dd\",\n            \"nameNl\": \"goulash met quorn en paprika dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"ajuin aanbakken in vetstof, quornblokjes toevoegen - tomatenpuree en paprikapoeder toevoegen en laten uitdrogen - bevochtigen met water en tomatenblokjes - kruiden met pezo, look, tijm en laurier - bouillon toevoegen champignons en paprika toevoegen. - binden met blanke roux en afwerken met peterselie.\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1318,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1318,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1318,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1318,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1353,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1971,\n          \"menuItemId\": 1353,\n          \"courseId\": 977,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 977,\n            \"dispNameNl\": \"couscous\",\n            \"dispNameEn\": \"couscous\",\n            \"nameNl\": \"couscous\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"doe de couscous in een diepe gastronorm. - kook het water met de bouillon - meng de overige groenten met de couscous - giet het water over de couscous en laat het geheel wellen - regelmatig losroeren\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 977,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 977,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1972,\n          \"menuItemId\": 1353,\n          \"courseId\": 1032,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1032,\n            \"dispNameNl\": \"couscousgroenten\",\n            \"dispNameEn\": \"couscous vegetables\",\n            \"nameNl\": \"couscousgroenten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"couscousgroenten beetgaar steamen. stoven in margarine, op smaak brengen met peper en zout. in gastronorm doen en bestrooien met koriander.\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1032,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1970,\n          \"menuItemId\": 1353,\n          \"courseId\": 1864,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1864,\n            \"dispNameNl\": \"Kiprollade met zongedroogde tomaat \",\n            \"dispNameEn\": \"Chicken roulade with sun-dried tomatoes \",\n            \"nameNl\": \"kiprollade zongedroogde tomaat dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"zie receptuur portioneren, schikken op geoliede gastro en verwarmen combi op 140\\u00b0 met vochtimpuls\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1864,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1864,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1864,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1427,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2087,\n          \"menuItemId\": 1427,\n          \"courseId\": 5177,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5177,\n            \"dispNameNl\": \"Penne met paddenstoelen en romige bloemkoolsaus\",\n            \"dispNameEn\": \"Penne with mushrooms and a creamy cauliflower sauce\",\n            \"nameNl\": \"00 penne met paddenstoelen en romige bloemkoolsaus (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de bloemkoolsaus klaar volgens de receptuur (zie fiche). - maak de volkoren penne klaar volgens de receptuur. fruit de ui en bak hierbij de champignons, breng op smaak met look, peper en zout.- meng de pasta met de bloemkoolsaus, de champignons en werk af met platte peterselie & gebakken uitjes.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5177,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1430,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2089,\n          \"menuItemId\": 1430,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2088,\n          \"menuItemId\": 1430,\n          \"courseId\": 1416,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1416,\n            \"dispNameNl\": \"African sunshinesaus\",\n            \"dispNameEn\": \"African sunshine sauce\",\n            \"nameNl\": \"african sunshine saus, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"courgettes en paprika aanstoven en kruiden. - mengen met de pasta en de african sunshine saus toevoegen.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1431,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2092,\n          \"menuItemId\": 1431,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2090,\n          \"menuItemId\": 1431,\n          \"courseId\": 3264,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3264,\n            \"dispNameNl\": \"Kipfilet op de grill \",\n            \"dispNameEn\": \"Grilled chicken breast \",\n            \"nameNl\": \"kipfilet op de grill 1 (kippenkruiden), dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2091,\n          \"menuItemId\": 1431,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1432,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 208,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2093,\n          \"menuItemId\": 1432,\n          \"courseId\": 5224,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5224,\n            \"dispNameNl\": \"Buddha bowl humuslicious\",\n            \"dispNameEn\": \"Buddha bow hummuslicious\",\n            \"nameNl\": \"00 buddha bowl humuslicious,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de quinoa klaar volgens de receptuur & laat afkoelen.- rooster de kikkererwten in de oven: besprenkel met olijfolie en kruid af met kruid mole oaxaca en zeezout & laat afkoelen - rooster de groene asperges - maak de bowl: doe de quinoa in een kom, vervolgens de gegrilde asperges, spinazie en schijfjes avocado (besprenkel met citroensap).- werk af met kerstomaten,granaatappelpitjes en de humus in het midden en geroosterde kikkererwten.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5224,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5224,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5224,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"raw.schema.json\"\n}\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cmi.parsed.expected.yaml",
    "content": "$test_case:\n  course_of_interest: 1390\n  reason: |\n    This response originally broke an assumption that \"maincourse\" has precedence over \"showFirst\".\n    As far as I know, course component ordering is based on:\n    1. \"showFirst\": true has precedence over false, otherwise:\n    2. \"maincourse\": true has precedence over false, otherwise:\n    3. (?) \"sortOrder\": lower values have precedence over higher values, otherwise:\n    4: The order in which items are returned by the API\n\n    However, I have not yet encountered a sortOrder other than 0 in the wild, and I'd need to check the official website\n    implementation to see if this is actually used.\ncampus: cmi\ndate: '2019-11-25'\nmenu:\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - VEGAN\n    name:\n      en: Falafel\n      nl: Falafel\n  - allergens:\n    - CELERY\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: sauce Marengo\n      nl: marengosaus\n  - allergens: [ ]\n    attributes: [ ]\n    name:\n      en: parsley potatoes\n      nl: aardappelen met peterselie\n  external_id: 838\n  multiple_prices: true\n  price: '4.00'\n  sort_order: 1\n- components:\n  - allergens:\n    - CELERY\n    - WHEAT_GLUTEN\n    attributes:\n    - SOUP\n    - VEGAN\n    name:\n      en: Leek soup\n      nl: Preisoep\n  external_id: 839\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    name:\n      en: \"Turkey pav\\xE9\"\n      nl: \"Kalkoenpav\\xE9\"\n  - allergens:\n    - CELERY\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: sauce Marengo\n      nl: marengosaus\n  - allergens: [ ]\n    attributes: [ ]\n    name:\n      en: parsley potatoes\n      nl: aardappelen met peterselie\n  external_id: 840\n  multiple_prices: true\n  price: '4.20'\n  sort_order: 2\n- components:\n  - allergens:\n    - EGG\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: penne\n      nl: Penne\n    $test_case:\n      showFirst: true\n      maincourse: false\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    attributes:\n    - PASTA\n    - VEGGIE\n    name:\n      en: African sunshine sauce\n      nl: African sunshinesaus\n    $test_case:\n      showFirst: false\n      maincourse: true\n  external_id: 1390\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: Penne with mushrooms and a creamy cauliflower sauce\n      nl: Penne met paddenstoelen en romige bloemkoolsaus\n  external_id: 1391\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens: [ ]\n    attributes:\n    - CHICKEN\n    - GRILL\n    name:\n      en: Marinated chicken skewer\n      nl: Gemarineerde kippenbrochette\n  - allergens:\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: \"Proven\\xE7al sauce\"\n      nl: \"Proven\\xE7aalse saus\"\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: fries\n      nl: frieten\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - LUPINE\n    - MILK_LACTOSE\n    - MOLLUSKS\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: saladbar\n      nl: Saladbar\n  external_id: 1392\n  multiple_prices: true\n  price: '5.20'\n  sort_order: 5\n- components:\n  - allergens:\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    - SALAD\n    name:\n      en: Mango and chicken salad\n      nl: Mango-kip-salade\n  external_id: 1393\n  multiple_prices: true\n  price: '4.40'\n  sort_order: 6\n- components:\n  - allergens:\n    - EGG\n    - FISH\n    - MUSTARD\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SALAD\n    name:\n      en: Salad with peaches and salmon salad\n      nl: Salade met perziken en zalmsalade\n  external_id: 1394\n  multiple_prices: true\n  price: '4.80'\n  sort_order: 7\n- components:\n  - allergens:\n    - EGG\n    - FISH\n    - MUSTARD\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SNACK\n    name:\n      en: Trout and citrus sandwich\n      nl: Broodje forel-citrus\n  external_id: 1396\n  multiple_prices: false\n  price: '3.10'\n  sort_order: 8\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cmi.raw.json",
    "content": "{\n  \"id\": 177,\n  \"menuDate\": \"2019-11-25T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 839,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1254,\n          \"menuItemId\": 839,\n          \"courseId\": 869,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 869,\n            \"dispNameNl\": \"Preisoep\",\n            \"dispNameEn\": \"Leek soup\",\n            \"nameNl\": \"preisoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 869,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 869,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 869,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 869,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: \\u20ac 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 838,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1252,\n          \"menuItemId\": 838,\n          \"courseId\": 924,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 924,\n            \"dispNameNl\": \"marengosaus \",\n            \"dispNameEn\": \"sauce Marengo \",\n            \"nameNl\": \"marengo saus dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux. - afsmaken met pezo en proven\\u00e7aalse kruiden en sambal. - garnituur van gestoofde ajuin, olijven, erwtjes en peterselie toevoegen opm je kan ook de erwtjes apart geven als volwaardige groenten (meer rekenen dan)\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 924,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 924,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1253,\n          \"menuItemId\": 838,\n          \"courseId\": 991,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 991,\n            \"dispNameNl\": \"aardappelen met peterselie\",\n            \"dispNameEn\": \"parsley potatoes\",\n            \"nameNl\": \"aardappelen met peterselie\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"stoom de aardappelen gaar in 18 min. - voeg de gehakte peterselie toe. -je kan er optioneel geklaarde boter aan toevoegen\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1251,\n          \"menuItemId\": 838,\n          \"courseId\": 1269,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1269,\n            \"dispNameNl\": \"Falafel\",\n            \"dispNameEn\": \"Falafel\",\n            \"nameNl\": \"falafel dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"6x14g\",\n            \"extra\": null,\n            \"preparation\": \"frituur voorverwarmen op 170\\u00b0. - falafel afbakken tot een temperatuur van minstens 65\\u00b0c bereikt is . - in bain marie schikken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1269,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1269,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 840,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1256,\n          \"menuItemId\": 840,\n          \"courseId\": 924,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 924,\n            \"dispNameNl\": \"marengosaus \",\n            \"dispNameEn\": \"sauce Marengo \",\n            \"nameNl\": \"marengo saus dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux. - afsmaken met pezo en proven\\u00e7aalse kruiden en sambal. - garnituur van gestoofde ajuin, olijven, erwtjes en peterselie toevoegen opm je kan ook de erwtjes apart geven als volwaardige groenten (meer rekenen dan)\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 924,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 924,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1257,\n          \"menuItemId\": 840,\n          \"courseId\": 991,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 991,\n            \"dispNameNl\": \"aardappelen met peterselie\",\n            \"dispNameEn\": \"parsley potatoes\",\n            \"nameNl\": \"aardappelen met peterselie\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"stoom de aardappelen gaar in 18 min. - voeg de gehakte peterselie toe. -je kan er optioneel geklaarde boter aan toevoegen\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1255,\n          \"menuItemId\": 840,\n          \"courseId\": 1841,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1841,\n            \"dispNameNl\": \"Kalkoenpav\\u00e9\",\n            \"dispNameEn\": \"Turkey pav\\u00e9\",\n            \"nameNl\": \"kalkoenpav\\u00e9, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"120g\",\n            \"extra\": null,\n            \"preparation\": \"op te warmen in steamer. - vacu\\u00fcm verpakt\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1841,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1841,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1391,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2029,\n          \"menuItemId\": 1391,\n          \"courseId\": 5177,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5177,\n            \"dispNameNl\": \"Penne met paddenstoelen en romige bloemkoolsaus\",\n            \"dispNameEn\": \"Penne with mushrooms and a creamy cauliflower sauce\",\n            \"nameNl\": \"00 penne met paddenstoelen en romige bloemkoolsaus (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de bloemkoolsaus klaar volgens de receptuur (zie fiche). - maak de volkoren penne klaar volgens de receptuur. fruit de ui en bak hierbij de champignons, breng op smaak met look, peper en zout.- meng de pasta met de bloemkoolsaus, de champignons en werk af met platte peterselie & gebakken uitjes.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5177,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1390,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2027,\n          \"menuItemId\": 1390,\n          \"courseId\": 1416,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1416,\n            \"dispNameNl\": \"African sunshinesaus\",\n            \"dispNameEn\": \"African sunshine sauce\",\n            \"nameNl\": \"african sunshine saus, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"courgettes en paprika aanstoven en kruiden. - mengen met de pasta en de african sunshine saus toevoegen.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2028,\n          \"menuItemId\": 1390,\n          \"courseId\": 5480,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5480,\n            \"dispNameNl\": \"Penne\",\n            \"dispNameEn\": \"penne\",\n            \"nameNl\": \"Penne,kookvast\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200 g pp\",\n            \"extra\": \"\",\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, olie toevoegen. De kooktijd is afhankelijk van de soort pasta.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5480,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5480,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1392,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2031,\n          \"menuItemId\": 1392,\n          \"courseId\": 932,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 932,\n            \"dispNameNl\": \"Proven\\u00e7aalse saus\",\n            \"dispNameEn\": \"Proven\\u00e7al sauce\",\n            \"nameNl\": \"proven\\u00e7aalse saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux - afsmaken met pezo en proven\\u00e7aalse kruiden. ajuin en champignons aanstoven en kleuren, courgetten (schijven nog eventueel halveren) en paprika toevoegen warme tomatensaus toevoegen en afwerken met proven\\u00e7aalse kruiden en peterselie. - eventueel verdunnen met heet water\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 932,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 932,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2033,\n          \"menuItemId\": 1392,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2030,\n          \"menuItemId\": 1392,\n          \"courseId\": 1374,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1374,\n            \"dispNameNl\": \"Gemarineerde kippenbrochette \",\n            \"dispNameEn\": \"Marinated chicken skewer \",\n            \"nameNl\": \"kippenbrochette gemarineerd, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"marineren en afbakken op de grill\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1374,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1374,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2032,\n          \"menuItemId\": 1392,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1393,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 2034,\n          \"menuItemId\": 1393,\n          \"courseId\": 3871,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3871,\n            \"dispNameNl\": \"Mango-kip-salade\",\n            \"dispNameEn\": \"Mango and chicken salad\",\n            \"nameNl\": \"salade mango-kip, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3871,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3871,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1394,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 2035,\n          \"menuItemId\": 1394,\n          \"courseId\": 2071,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2071,\n            \"dispNameNl\": \"Salade met perziken en zalmsalade\",\n            \"dispNameEn\": \"Salad with peaches and salmon salad\",\n            \"nameNl\": \"salade met perziken en zalmsalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"zalm stomen en laten afkoelen (slechts max. 100g per persoon gebruiken) - zalm prakken, mengen met een kleine hoeveelheid mayonaise (voeg enkel extra mayo toe als de zalmsalade nog te droog is, laat de zalm in geen geval \\u2018zwemmen\\u2019 in de mayonaise), met geplette hardgekookte eieren en peterselie - perziken in partjes snijden - veldsla en rammenas mengen. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken volgorde voor vullen van onder naar boven: gesneden perziken (2 -3 stuks) + zalmsalade (+/- 80-100 gr) + gemengde veldsla \\u2013 rammenas\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1396,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 177,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 2037,\n          \"menuItemId\": 1396,\n          \"courseId\": 3846,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3846,\n            \"dispNameNl\": \"Broodje forel-citrus\",\n            \"dispNameEn\": \"Trout and citrus sandwich\",\n            \"nameNl\": \"broodje forel-citrus, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"citrus vinaigrette mengen met mayo, dille, peterselie, veldsla en forel - op bun broodje beleggen\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3846,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3846,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3846,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 3846,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3846,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3846,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-11-25_cmu.parsed.expected.yaml",
    "content": "campus: cmu\ndate: '2019-11-25'\nmenu:\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Oriental bulghur salad with chilli\n      nl: Oosterse bulgursalade met chili\n  external_id: 1321\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 1\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SALAD\n    - VEGGIE\n    name:\n      en: Crunchy salad\n      nl: Krokante salade\n  external_id: 1322\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 2\n- components:\n  - allergens: []\n    attributes:\n    - BIO\n    - SOUP\n    - VEGAN\n    name:\n      en: Organic broccoli soup\n      nl: Bio-broccolisoep\n  external_id: 1323\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SNACK\n    - VEGGIE\n    name:\n      en: Grilled goat cheese sandwich with honey\n      nl: Croque geitenkaas-honing\n  external_id: 1324\n  multiple_prices: false\n  price: '1.60'\n  sort_order: 11\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: Panini California\n      nl: Panini California\n  external_id: 1325\n  multiple_prices: false\n  price: '2.90'\n  sort_order: 3\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cmu.raw.json",
    "content": "{\n  \"id\": 242,\n  \"menuDate\": \"2019-11-25T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1323,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 242,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1943,\n          \"menuItemId\": 1323,\n          \"courseId\": 2524,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2524,\n            \"dispNameNl\": \"Bio-broccolisoep\",\n            \"dispNameEn\": \"Organic broccoli soup\",\n            \"nameNl\": \"bio-broccolisoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2524,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2524,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2524,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1321,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 242,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1941,\n          \"menuItemId\": 1321,\n          \"courseId\": 4980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4980,\n            \"dispNameNl\": \"Oosterse bulgursalade met chili\",\n            \"dispNameEn\": \"Oriental bulghur salad with chilli\",\n            \"nameNl\": \"00 oosterse bulgursalade met chili,w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4980,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4980,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4980,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4980,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4980,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4980,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1322,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 242,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1942,\n          \"menuItemId\": 1322,\n          \"courseId\": 3142,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3142,\n            \"dispNameNl\": \"Krokante salade\",\n            \"dispNameEn\": \"Crunchy salad\",\n            \"nameNl\": \"krokante salade (veggie), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"laagjes van onder naar boven: noten (mengeling) rammenas appel met schil (in schijfjes), besprenkelen met citroensap witloofblaren blauwe kaas veldsla croutons potje dressing\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 3142,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3142,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3142,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3142,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1325,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 242,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 1945,\n          \"menuItemId\": 1325,\n          \"courseId\": 4972,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4972,\n            \"dispNameNl\": \"Panini California \",\n            \"dispNameEn\": \"Panini California \",\n            \"nameNl\": \"00 california panini,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"snack winter olie van de tomaatjes maakt het smeuig\",\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4972,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4972,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4972,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4972,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4972,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1324,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 242,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 1944,\n          \"menuItemId\": 1324,\n          \"courseId\": 1082,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1082,\n            \"dispNameNl\": \"Croque geitenkaas-honing\",\n            \"dispNameEn\": \"Grilled goat cheese sandwich with honey\",\n            \"nameNl\": \"croque geitenkaas-honing, dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"keuze uit wit brood of bruin brood om de croque te maken 1 potje saus is in de prijs inbegrepen\",\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1082,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1082,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1082,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1082,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1082,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-11-25_cst.parsed.expected.yaml",
    "content": "campus: cst\ndate: '2019-11-25'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes:\n    - SOUP\n    - VEGGIE\n    name:\n      en: Parsnip soup\n      nl: Pastinaaksoep\n  external_id: 982\n  multiple_prices: false\n  price: '1.50'\n  sort_order: 0\n- components:\n  - allergens: []\n    attributes:\n    - VEGGIE\n    name:\n      en: Pea kofte\n      nl: \"Erwtenk\\xF6fteballetjes\"\n  - allergens:\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: white cabbage in white sauce\n      nl: witte kool in witte saus\n  - allergens: []\n    attributes: []\n    name:\n      en: fried potato slices\n      nl: gebakken aardappelschijfjes\n  external_id: 983\n  multiple_prices: true\n  price: '4.40'\n  sort_order: 1\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - LESS_MEAT\n    - PIG\n    name:\n      en: Less Meat Loaf\n      nl: Vleesbrood met weinig vlees\n  - allergens:\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: white cabbage in white sauce\n      nl: witte kool in witte saus\n  - allergens: []\n    attributes: []\n    name:\n      en: fried potato slices\n      nl: gebakken aardappelschijfjes\n  external_id: 984\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 2\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: Penne with mushrooms and a creamy cauliflower sauce\n      nl: Penne met paddenstoelen en romige bloemkoolsaus\n  external_id: 1412\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: spelt penne\n      nl: Speltpenne\n  - allergens:\n    - CELERY\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: bolnienaise veganlicious\n      nl: bolnienaise veganlicious\n  external_id: 1413\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens: []\n    attributes:\n    - CHICKEN\n    - GRILL\n    name:\n      en: Grilled chicken breast\n      nl: Kipfilet op de grill\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - SOY\n    attributes: []\n    name:\n      en: choron sauce\n      nl: choronsaus\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - LUPINE\n    - MILK_LACTOSE\n    - MOLLUSKS\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: saladbar\n      nl: Saladbar\n  external_id: 1414\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 5\n- components:\n  - allergens:\n    - CELERY\n    - NUTS\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: \"P\\xE9pites bowl\"\n      nl: \"P\\xE9pites bowl\"\n  external_id: 1415\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 11\n- components:\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SALAD\n    - VEGGIE\n    name:\n      en: Cottage cheese salad\n      nl: \"Salade h\\xFCttekase\"\n  external_id: 1416\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 11\n- components:\n  - allergens:\n    - CELERY\n    - SOY\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Thai Bombai salad\n      nl: Thai bombai salade\n  external_id: 1417\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 11\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_cst.raw.json",
    "content": "{\n  \"id\": 194,\n  \"menuDate\": \"2019-11-25T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 982,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1543,\n          \"menuItemId\": 982,\n          \"courseId\": 867,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 867,\n            \"dispNameNl\": \"Pastinaaksoep\",\n            \"dispNameEn\": \"Parsnip soup\",\n            \"nameNl\": \"pastinaaksoep, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 867,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 867,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 867,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 867,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 867,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 983,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1510,\n          \"menuItemId\": 983,\n          \"courseId\": 1068,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1068,\n            \"dispNameNl\": \"witte kool in witte saus\",\n            \"dispNameEn\": \"white cabbage in white sauce\",\n            \"nameNl\": \"witte kool in witte saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"witte kool steamen, witte basissaus maken (zie receptuur) - mengen - kruiden met pezono.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1068,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1068,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1509,\n          \"menuItemId\": 983,\n          \"courseId\": 1277,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1277,\n            \"dispNameNl\": \"Erwtenk\\u00f6fteballetjes\",\n            \"dispNameEn\": \"Pea kofte\",\n            \"nameNl\": \"erwtenk\\u00f6fte balletjes dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"6 x 22g\",\n            \"extra\": null,\n            \"preparation\": \"friteuse verhitten tot 160\\u00b0c. - kroketjes afbakken tot een temperatuur van minstens 65\\u00b0c bereikt is . - in bain-marie schikken.\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1277,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1511,\n          \"menuItemId\": 983,\n          \"courseId\": 1985,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1985,\n            \"dispNameNl\": \"gebakken aardappelschijfjes\",\n            \"dispNameEn\": \"fried potato slices\",\n            \"nameNl\": \"gebakken aardappel schijfjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in margarine en olijfolie\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 984,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1513,\n          \"menuItemId\": 984,\n          \"courseId\": 1068,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1068,\n            \"dispNameNl\": \"witte kool in witte saus\",\n            \"dispNameEn\": \"white cabbage in white sauce\",\n            \"nameNl\": \"witte kool in witte saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"witte kool steamen, witte basissaus maken (zie receptuur) - mengen - kruiden met pezono.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1068,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1068,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1514,\n          \"menuItemId\": 984,\n          \"courseId\": 1985,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1985,\n            \"dispNameNl\": \"gebakken aardappelschijfjes\",\n            \"dispNameEn\": \"fried potato slices\",\n            \"nameNl\": \"gebakken aardappel schijfjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in margarine en olijfolie\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1512,\n          \"menuItemId\": 984,\n          \"courseId\": 2062,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2062,\n            \"dispNameNl\": \"Vleesbrood met weinig vlees\",\n            \"dispNameEn\": \"Less  Meat Loaf\",\n            \"nameNl\": \"vleesbrood met weinig vleesch, dd, z & w - MINDER VLEES\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g pp\",\n            \"extra\": \"\",\n            \"preparation\": \"bonen spoelen en laten uitlekken - bonen fijn cutteren - het gehakte vleesch :-) mengen met de gecutterde bonen, chapelure en ei en kruiden met pezono naar smaak. - maak er een gehaktbrood van (of balletjes indien haalbaar in jouw keuken of deze periode) - garen in oven . - gehaktbrood in sneetjes snijden\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2062,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2062,\n                \"courseLogoId\": 212\n              },\n              {\n                \"courseId\": 2062,\n                \"courseLogoId\": 216\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1412,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2063,\n          \"menuItemId\": 1412,\n          \"courseId\": 5177,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5177,\n            \"dispNameNl\": \"Penne met paddenstoelen en romige bloemkoolsaus\",\n            \"dispNameEn\": \"Penne with mushrooms and a creamy cauliflower sauce\",\n            \"nameNl\": \"00 penne met paddenstoelen en romige bloemkoolsaus (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de bloemkoolsaus klaar volgens de receptuur (zie fiche). - maak de volkoren penne klaar volgens de receptuur. fruit de ui en bak hierbij de champignons, breng op smaak met look, peper en zout.- meng de pasta met de bloemkoolsaus, de champignons en werk af met platte peterselie & gebakken uitjes.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5177,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5177,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1413,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2064,\n          \"menuItemId\": 1413,\n          \"courseId\": 5053,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5053,\n            \"dispNameNl\": \"bolnienaise veganlicious\",\n            \"dispNameEn\": \"bolnienaise veganlicious\",\n            \"nameNl\": \"00 bolnienaise veganlicious z&w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200 gr\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"pasta - kook de linzen apart - stoof de ui aan samen met de paprika, wortel en selder - voeg de tomatenpulp, puree, kruiden, bouillon toe en laat koken - voeg de linzen toe en laat nog even meekoken.- breng op smaak en bind met de roux. - werk af met platte peterselie en cashewnoten\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5053,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5053,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5053,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2065,\n          \"menuItemId\": 1413,\n          \"courseId\": 5487,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5487,\n            \"dispNameNl\": \"Speltpenne\",\n            \"dispNameEn\": \"spelt penne\",\n            \"nameNl\": \"Speltpenne (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Kook de pasta gaar in licht gezouten water, olie toevoegen. de kooktijd is afhankelijk van de soort pasta.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5487,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1414,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2067,\n          \"menuItemId\": 1414,\n          \"courseId\": 908,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 908,\n            \"dispNameNl\": \"choronsaus\",\n            \"dispNameEn\": \"choron sauce\",\n            \"nameNl\": \"choron saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"tomatino onder warme b\\u00e9arnaisesaus mengen\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 908,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2066,\n          \"menuItemId\": 1414,\n          \"courseId\": 3264,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3264,\n            \"dispNameNl\": \"Kipfilet op de grill \",\n            \"dispNameEn\": \"Grilled chicken breast \",\n            \"nameNl\": \"kipfilet op de grill 1 (kippenkruiden), dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2068,\n          \"menuItemId\": 1414,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1415,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2069,\n          \"menuItemId\": 1415,\n          \"courseId\": 5529,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5529,\n            \"dispNameNl\": \"P\\u00e9pites bowl\",\n            \"dispNameEn\": \"P\\u00e9pites bowl\",\n            \"nameNl\": \"P\\u00e9pites bowl,w, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de p\\u00e9pites klaar volgens de bereidingswijze op de verpakking & laat afkoelen . - Maak de aziatische tuinerwtenspread volgens de receptuur. Maak de budhabowl als volgt: vul het kommetje met de p\\u00e9pites, vervolgens de wortelstaafjes, edamame, witte & gele raapjes..Werk de budha bowl af met een toefje tuinerwtenspread in het midden, waterkers, kimchi & sesamzaad. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5529,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5529,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5529,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5529,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5529,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5529,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5529,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1416,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2070,\n          \"menuItemId\": 1416,\n          \"courseId\": 4167,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4167,\n            \"dispNameNl\": \"Salade h\\u00fcttekase\",\n            \"dispNameEn\": \"Cottage cheese salad\",\n            \"nameNl\": \"salade h\\u00fcttekase, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"van onder naar boven: cottage cheese - dan laagje piquillo- boontjes, zoete aardappelen, peper, rode ui ringen, ei, waterkers, cashewnoot, croutons.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4167,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4167,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4167,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4167,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1417,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 194,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2071,\n          \"menuItemId\": 1417,\n          \"courseId\": 3490,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3490,\n            \"dispNameNl\": \"Thai bombai salade\",\n            \"dispNameEn\": \"Thai Bombai salad\",\n            \"nameNl\": \"thai bombai salade, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"conceptsalade winter miehoen koken - mie mengen met de world grill saus - pastinaak grillen en mengen met de wortelen en sojascheuten en kokosschilfers - opbouw: miehoen, groentjes en platte peterselie\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3490,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3490,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3490,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3490,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-11-25_hzs.parsed.expected.yaml",
    "content": "campus: hzs\ndate: '2019-11-25'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - WHEAT_GLUTEN\n    attributes:\n    - SOUP\n    - VEGAN\n    name:\n      en: Bell pepper soup\n      nl: Paprikasoep\n  external_id: 1163\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGGIE\n    name:\n      en: Sweet 'n cheesy\n      nl: Sweet 'n cheesy\n  external_id: 1164\n  multiple_prices: false\n  price: '3.10'\n  sort_order: 1\n- components:\n  - allergens:\n    - EGG\n    - FISH\n    - MUSTARD\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SNACK\n    name:\n      en: Multigrain roll with langoustine salad\n      nl: Meergranenbroodje met langoustinesalade\n  external_id: 1165\n  multiple_prices: false\n  price: '3.10'\n  sort_order: 2\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    - MUSTARD\n    - WHEAT_GLUTEN\n    attributes:\n    - SALAD\n    - VEGGIE\n    name:\n      en: Marrakesh salad\n      nl: Salade Marrakech\n  external_id: 1166\n  multiple_prices: true\n  price: '4.40'\n  sort_order: 3\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Avocado and quinoa salad\n      nl: Avocado-quinoasalade\n  external_id: 1167\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 10\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: Mexican wrap\n      nl: Mexicaanse wrap\n  external_id: 1168\n  multiple_prices: false\n  price: '2.00'\n  sort_order: 10\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: Humus and avocado panini\n      nl: Humus-avocado-panini\n  external_id: 1169\n  multiple_prices: false\n  price: '3.60'\n  sort_order: 10\n- components:\n  - allergens: []\n    attributes:\n    - PASTA\n    - VEGGIE\n    name:\n      en: Pasta pesto with sun-dried tomatoes\n      nl: Pasta pesto met zongedroogde tomaatjes\n  external_id: 1170\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 10\n"
  },
  {
    "path": "tests/external_menus/2019-11-25_hzs.raw.json",
    "content": "{\n  \"id\": 227,\n  \"menuDate\": \"2019-11-25T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1163,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1738,\n          \"menuItemId\": 1163,\n          \"courseId\": 864,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 864,\n            \"dispNameNl\": \"Paprikasoep\",\n            \"dispNameEn\": \"Bell pepper soup\",\n            \"nameNl\": \"paprikasoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 864,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 864,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 864,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 864,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1164,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1739,\n          \"menuItemId\": 1164,\n          \"courseId\": 403,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 403,\n            \"dispNameNl\": \"Sweet 'n cheesy \",\n            \"dispNameEn\": \"Sweet 'n cheesy \",\n            \"nameNl\": \"sweet 'n cheesy (smeerkaas, tapenade paprika) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 403,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 403,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 403,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 403,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1165,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1740,\n          \"menuItemId\": 1165,\n          \"courseId\": 1884,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1884,\n            \"dispNameNl\": \"Meergranenbroodje met langoustinesalade \",\n            \"dispNameEn\": \"Multigrain roll with langoustine salad \",\n            \"nameNl\": \"langoustinesalade (smos), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"zie fiche langoustinesalade bij belegde broodjes (eenvoudig) en met zomer- en wintergroentjes scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1884,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1884,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1884,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1166,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 1741,\n          \"menuItemId\": 1166,\n          \"courseId\": 281,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 281,\n            \"dispNameNl\": \"Salade Marrakech \",\n            \"dispNameEn\": \"Marrakesh salad \",\n            \"nameNl\": \"salade marrakech (couscous, mozzarella), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 281,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 281,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 281,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 281,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 281,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 281,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1167,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 1742,\n          \"menuItemId\": 1167,\n          \"courseId\": 4978,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4978,\n            \"dispNameNl\": \"Avocado-quinoasalade\",\n            \"dispNameEn\": \"Avocado and quinoa salad\",\n            \"nameNl\": \"avocado-quinoasalade, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 gr\",\n            \"extra\": null,\n            \"preparation\": \"conceptsalade winter kook de quinoa in de groentebouillon - steam de pompoenblokjes en laat afkoelen - snij de avocado in mooie blokjes en snipper de koriander- laat de rode bonen uitlekken - vermeng de groenten, de ajuin,de mais, de rode bonen, de noten, de koriander met ma\\u00efsolie, sap van limoen en pezo - en meng dit met de quinoa. - vul de saladebox met de quinoasalade.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4978,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4978,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1168,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 1743,\n          \"menuItemId\": 1168,\n          \"courseId\": 5075,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5075,\n            \"dispNameNl\": \"Mexicaanse wrap \",\n            \"dispNameEn\": \"Mexican wrap \",\n            \"nameNl\": \"00 mexicaanse wrap z&w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"meng de salsa mexicana mix met de ma\\u00efs - beleg de wrap in het midden met de ontdooide guacamole, vervolgens met de mexicaanse ma\\u00efs, gesneden pijpajuin, koriander, beetje sambal en de gepelde rode paprika.- leg de wrap tussen de panini machine voor circa 5 minuten.\",\n            \"price\": 2.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5075,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5075,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5075,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1169,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 1744,\n          \"menuItemId\": 1169,\n          \"courseId\": 4973,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4973,\n            \"dispNameNl\": \"Humus-avocado-panini \",\n            \"dispNameEn\": \"Humus and avocado panini\",\n            \"nameNl\": \"00 panini humus avocado, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"snack winter\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4973,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4973,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4973,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4973,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4973,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1170,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 227,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 1745,\n          \"menuItemId\": 1170,\n          \"courseId\": 4144,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4144,\n            \"dispNameNl\": \"Pasta pesto met zongedroogde tomaatjes\",\n            \"dispNameEn\": \"Pasta pesto with sun-dried tomatoes\",\n            \"nameNl\": \"pasta pesto met zongedroogde tomaatjes, hzs\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4144,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 4144,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_cde.parsed.expected.yaml",
    "content": "campus: cde\ndate: '2019-12-12'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - MOLLUSKS\n    - SESAME\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    - FISH\n    name:\n      en: Chicken and seafood paella\n      nl: \"Pa\\xEBlla met kip en zeevruchten\"\n  external_id: 1778\n  multiple_prices: true\n  price: '4.60'\n  sort_order: 2\n- components:\n  - allergens:\n    - NUTS\n    - PEANUTS\n    - SESAME\n    attributes:\n    - CHICKEN\n    - SALAD\n    name:\n      en: Sweet potato bowl\n      nl: Sweet potato bowl\n  external_id: 1794\n  multiple_prices: true\n  price: '4.00'\n  sort_order: 11\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SALAD\n    - VEGGIE\n    name:\n      en: New York salad\n      nl: Salade New York\n  external_id: 1795\n  multiple_prices: true\n  price: '4.00'\n  sort_order: 11\n- components:\n  - allergens: []\n    attributes:\n    - BIO\n    - SOUP\n    - VEGAN\n    name:\n      en: Organic spinach soup\n      nl: Bio-spinaziesoep\n  external_id: 1843\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: wholegrain spaghetti\n      nl: Volkoren spaghetti\n  - allergens:\n    - CELERY\n    - PEANUTS\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: soy bolognese sauce\n      nl: soja-bolognaisesaus\n  external_id: 1976\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens:\n    - EGG\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: fusili\n      nl: Fusili\n  - allergens:\n    - MILK_LACTOSE\n    - NUTS\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    name:\n      en: Oriental turkey\n      nl: Oriental turkey\n  external_id: 1977\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens:\n    - MUSTARD\n    attributes:\n    - GRILL\n    - PIG\n    name:\n      en: Marinated pork rib\n      nl: Varkensrib gemarineerd\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: fries\n      nl: frieten\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - LUPINE\n    - MILK_LACTOSE\n    - MOLLUSKS\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: saladbar\n      nl: Saladbar\n  external_id: 1978\n  multiple_prices: true\n  price: '4.80'\n  sort_order: 5\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    attributes:\n    - CHEESE\n    - PASTA\n    - VEGGIE\n    name:\n      en: Ravioli verdura\n      nl: Ravioli verdura\n  external_id: 2325\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 1\n"
  },
  {
    "path": "tests/external_menus/2019-12-12_cde.processed.expected.yaml",
    "content": "campus: cde\ndate: '2019-12-12'\nmenu:\n- course_allergens:\n  - CELERY\n  - EGG\n  - FISH\n  - MOLLUSKS\n  - SESAME\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  - FISH\n  course_sub_type: NORMAL\n  course_type: DAILY\n  external_id: 1778\n  name:\n    en: Chicken and seafood paella\n    nl: \"Pa\\xEBlla met kip en zeevruchten\"\n  price_staff: '5.70'\n  price_students: '4.60'\n- course_allergens:\n  - NUTS\n  - PEANUTS\n  - SESAME\n  course_attributes:\n  - CHICKEN\n  - SALAD\n  course_sub_type: NORMAL\n  course_type: SALAD\n  external_id: 1794\n  name:\n    en: Sweet potato bowl\n    nl: Sweet potato bowl\n  price_staff: '5.00'\n  price_students: '4.00'\n- course_allergens:\n  - CELERY\n  - EGG\n  - MILK_LACTOSE\n  - NUTS\n  - PEANUTS\n  - SESAME\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHEESE\n  - SALAD\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: SALAD\n  external_id: 1795\n  name:\n    en: New York salad\n    nl: Salade New York\n  price_staff: '5.00'\n  price_students: '4.00'\n- course_allergens: []\n  course_attributes:\n  - BIO\n  - SOUP\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: SOUP\n  external_id: 1843\n  name:\n    en: Organic spinach soup\n    nl: Bio-spinaziesoep\n  price_staff: null\n  price_students: '0.90'\n- course_allergens:\n  - CELERY\n  - PEANUTS\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - PASTA\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: PASTA\n  external_id: 1976\n  name:\n    en: Wholegrain spaghetti, soy bolognese sauce\n    nl: Volkoren spaghetti, soja-bolognaisesaus\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - EGG\n  - MILK_LACTOSE\n  - NUTS\n  - SESAME\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  course_sub_type: NORMAL\n  course_type: PASTA\n  external_id: 1977\n  name:\n    en: Fusili, Oriental turkey\n    nl: Fusili, Oriental turkey\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - CELERY\n  - EGG\n  - FISH\n  - LUPINE\n  - MILK_LACTOSE\n  - MOLLUSKS\n  - MUSTARD\n  - NUTS\n  - PEANUTS\n  - SESAME\n  - SHELLFISH\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - GRILL\n  - PIG\n  course_sub_type: NORMAL\n  course_type: GRILL\n  external_id: 1978\n  name:\n    en: Marinated pork rib, fries, saladbar\n    nl: Varkensrib gemarineerd, frieten, Saladbar\n  price_staff: '6.00'\n  price_students: '4.80'\n- course_allergens:\n  - MILK_LACTOSE\n  course_attributes:\n  - CHEESE\n  - PASTA\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: PASTA\n  external_id: 2325\n  name:\n    en: Ravioli verdura\n    nl: Ravioli verdura\n  price_staff: '6.20'\n  price_students: '5.00'\n"
  },
  {
    "path": "tests/external_menus/2019-12-12_cde.raw.json",
    "content": "{\n  \"id\": 286,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1843,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 2696,\n          \"menuItemId\": 1843,\n          \"courseId\": 2529,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2529,\n            \"dispNameNl\": \"Bio-spinaziesoep\",\n            \"dispNameEn\": \"Organic spinach soup\",\n            \"nameNl\": \"bio-spinaziesoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2529,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2529,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2529,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2325,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3362,\n          \"menuItemId\": 2325,\n          \"courseId\": 3568,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3568,\n            \"dispNameNl\": \"Ravioli verdura\",\n            \"dispNameEn\": \"Ravioli verdura\",\n            \"nameNl\": \"ravioli verdura, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": \"veggie\",\n            \"preparation\": null,\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3568,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3568,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3568,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 3568,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1778,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 2588,\n          \"menuItemId\": 1778,\n          \"courseId\": 1455,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1455,\n            \"dispNameNl\": \"Pa\\u00eblla met kip en zeevruchten\",\n            \"dispNameEn\": \"Chicken and seafood paella\",\n            \"nameNl\": \"paella met kip en zeevruchten, dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"rijst half gaar koken in water met curcuma - erwtjes en paprika beetgaar voorsteamen. - kip en visbouillon oplossen in witte wijn samen met pezo en lookpasta. - rijst, kip, groenten, mosselvlees, surimiflakes, bouillonmix samen in pan . - mengen en even opbakken - in gastro's van 6cm scheppen en in combi steam (150\\u00b0)opwarmen tot gewenste temp. - uitscheppen en afwerken met 2 ringetjes gefrituurde calamares, citroentje en peterselie en de vooraf gebakken kippenonderboutjes,\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1455,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1455,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1976,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2876,\n          \"menuItemId\": 1976,\n          \"courseId\": 1121,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1121,\n            \"dispNameNl\": \"soja-bolognaisesaus \",\n            \"dispNameEn\": \"soy bolognese sauce \",\n            \"nameNl\": \"soja - bolognaise saus dd (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"pastasaus vege gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux . alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1121,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1121,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2877,\n          \"menuItemId\": 1976,\n          \"courseId\": 5574,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5574,\n            \"dispNameNl\": \"Volkoren spaghetti \",\n            \"dispNameEn\": \"wholegrain spaghetti\",\n            \"nameNl\": \"Volkoren spaghetti, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5574,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": false,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1977,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2878,\n          \"menuItemId\": 1977,\n          \"courseId\": 3106,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3106,\n            \"dispNameNl\": \"Oriental turkey\",\n            \"dispNameEn\": \"Oriental turkey\",\n            \"nameNl\": \"pastasaus oriental turkey, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": \"kebab ontdooien in de oven - en toevoegen bij de voorgekookte pasta - de courgetteschijven op smaak brengen met peper en zout en olijfolie, op een hete temperatuur in de oven bakken - olie, indian mystery, gebakken courgette - en paprika blokjes toevoegen - alles mengen en regenereren op 140 graden . overstrooien met zonnebloempitten en gesnipperde peterselie\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3106,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3106,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3106,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3106,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3106,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3106,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2879,\n          \"menuItemId\": 1977,\n          \"courseId\": 5476,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5476,\n            \"dispNameNl\": \"Fusili\",\n            \"dispNameEn\": \"fusili\",\n            \"nameNl\": \"Fusili\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": \"\",\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, olie toevoegen. de kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5476,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5476,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1978,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2882,\n          \"menuItemId\": 1978,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2880,\n          \"menuItemId\": 1978,\n          \"courseId\": 1080,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1080,\n            \"dispNameNl\": \"Varkensrib gemarineerd\",\n            \"dispNameEn\": \"Marinated pork rib\",\n            \"nameNl\": \"varkensrib gemarineerd\",\n            \"nameEn\": \"\",\n            \"weight\": \"180g\",\n            \"extra\": null,\n            \"preparation\": \"marineren en grillen\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1080,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1080,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 1080,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2881,\n          \"menuItemId\": 1978,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1794,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2607,\n          \"menuItemId\": 1794,\n          \"courseId\": 5531,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5531,\n            \"dispNameNl\": \"Sweet potato bowl\",\n            \"dispNameEn\": \"Sweet potato bowl\",\n            \"nameNl\": \"Sweet potato bowl, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de quinoa klaar volgens de bereidingswijze van de fiche, laat de quinoa afkoelen.  Maak de zoete aardappel klaar volgens de fiche en breng op smaak met kaneel. Maak de bowl: doe de quinoa in een kommetje, vervolgens de bonen, zoete aardappel, curly kale & raapjes.  Werk af met kipreepjes, cashewnoten, platte peterselie & furikake. Serveer hierbij de vinaigrette.Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1795,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 286,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2608,\n          \"menuItemId\": 1795,\n          \"courseId\": 274,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 274,\n            \"dispNameNl\": \"Salade New York\",\n            \"dispNameEn\": \"New York salad\",\n            \"nameNl\": \"salade new york (appel, noten, brie), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton citroensap besprenkelen over de appelstukjes\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 274,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_cgb.raw.json",
    "content": "{\n  \"id\": 317,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2081,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 3012,\n          \"menuItemId\": 2081,\n          \"courseId\": 2525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2525,\n            \"dispNameNl\": \"Bio-champignonsoep\",\n            \"dispNameEn\": \"Organic mushroom soup\",\n            \"nameNl\": \"bio-champignonsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2525,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2083,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3014,\n          \"menuItemId\": 2083,\n          \"courseId\": 4988,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4988,\n            \"dispNameNl\": \"Groene pasta met pistachenoten\",\n            \"dispNameEn\": \"Green paste with pistachio nuts\",\n            \"nameNl\": \"00 groene pasta met pistachenoten,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan (geen andere pasta, anders niet meer vegan)\",\n            \"preparation\": \"pasta kook de erwten in de bouillon gedurende 15 min. en giet ze af. steam de spinazie. (hou nog een deel van de spinazie en de erwtjes opzij voor de afwerking).voeg de erwtjes, spinazie en room samen.- pureer alles tot een gladde saus.- breng op smaak met kerrie en pezo.- meng de groene saus met de pasta en voeg er ook nog enkele erwtjes en spinazie onder. crush de pistachenoten - werk af met platte peterselie en gecrushte pistachenoten.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4988,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 4988,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2082,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 3013,\n          \"menuItemId\": 2082,\n          \"courseId\": 274,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 274,\n            \"dispNameNl\": \"Salade New York\",\n            \"dispNameEn\": \"New York salad\",\n            \"nameNl\": \"salade new york (appel, noten, brie), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton citroensap besprenkelen over de appelstukjes\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 274,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2084,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3015,\n          \"menuItemId\": 2084,\n          \"courseId\": 2424,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2424,\n            \"dispNameNl\": \"Panini 'kalkoen-pesto'\",\n            \"dispNameEn\": \"Turkey pesto panini\",\n            \"nameNl\": \"panini 'kalkoen-pesto', z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": null,\n            \"preparation\": \"vanaf nu pestospread gran'olivia gebruiken - sandwich spread opwerken\",\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2085,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 3016,\n          \"menuItemId\": 2085,\n          \"courseId\": 159,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 159,\n            \"dispNameNl\": \"Abdijbroodje \",\n            \"dispNameEn\": \"Abbey roll \",\n            \"nameNl\": \"abdijbroodje (baguelino broodje, smeerkaas, rammenas) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 159,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 159,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2326,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 10,\n      \"menuItemContents\": []\n    },\n    {\n      \"id\": 2330,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 317,\n      \"sortorder\": 10,\n      \"menuItemContents\": []\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_cmi.raw.json",
    "content": "{\n  \"id\": 240,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1306,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 1918,\n          \"menuItemId\": 1306,\n          \"courseId\": 2525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2525,\n            \"dispNameNl\": \"Bio-champignonsoep\",\n            \"dispNameEn\": \"Organic mushroom soup\",\n            \"nameNl\": \"bio-champignonsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2525,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2525,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1308,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 1927,\n          \"menuItemId\": 1308,\n          \"courseId\": 974,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 974,\n            \"dispNameNl\": \"aardappelen met bieslook\",\n            \"dispNameEn\": \"potatoes and chives\",\n            \"nameNl\": \"aardappelen met bieslook\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"de aardappelen gaar stomen (ongeveer 20 min). - bieslook versnipperen en mengen met de aardappelen. - de margarine klaren en mengen met de aardappelen (optioneel)\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1923,\n          \"menuItemId\": 1308,\n          \"courseId\": 1471,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1471,\n            \"dispNameNl\": \"Gegratineerd witloofpannetje\",\n            \"dispNameEn\": \"Belgian endive au gratin\",\n            \"nameNl\": \"gegratineerd witloofpannetje (+ puree met tuinkers), zvv, dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"kroon witloof verwijderen - witloof in geperforeerde gastronormen leggen en kortsteamen tot 3/4 gaarheid. laten afkoelen en uitlekken. witloof op smaak brengen met peper en zout. de courgetteblokjes steamen tot 3/4 gaarheid, laten afkoelen en op smaak brengen met peper en zout - 20 liter water aan de kook brengen. basissaus roerend oplossen in kokend water room toevoegen. -afsmaken met peper zout en citroensap en eventueel verder binden met roux. - gesloten 6 cm gastro bodem bedekken met laagje witte saus. hierop witloof en courgetteblokjes op schikken. als witloof geschikt is in gastroplaten verder oversausen.afwerken met gemalen kaas paneermeel en boterblokjes gratineren verwarmen in voorverwarmde oven 180\\u00b0 tot gewenste kern bereikt is. - afwerken met zonnebloempitjes en gehakte peterselie - serveren met tuinkerspuree zelfbereid (zie receptuur).\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1471,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1471,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1313,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 1926,\n          \"menuItemId\": 1313,\n          \"courseId\": 971,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 971,\n            \"dispNameNl\": \"aardappelgratin\",\n            \"dispNameEn\": \"potato gratin\",\n            \"nameNl\": \"gratin van aardappel\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": \"gratin dauphinois is als alternatief te gebruiken\",\n            \"preparation\": \"de aardappelen 3/4 gaarstomen. - saus maken met pezono- 4 gastronorm inboteren - aardappelen overgieten met witte saus - bestrooien met geraspte kaas - afbakken op 165 graden gedurende 30 min. - gratin dauphinois is als alternatief te gebruiken\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 971,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 971,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 1925,\n          \"menuItemId\": 1313,\n          \"courseId\": 2044,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2044,\n            \"dispNameNl\": \"Fazantenballetjes in portosaus en gebakken witloof\",\n            \"dispNameEn\": \"Pheasant meatballs in port sauce with braised Belgian endive\",\n            \"nameNl\": \"fazantenballetjes in portosaus (+ gebakken witloof), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"balletjes aanbakken en de ajuin toevoegen en mooi bruin laten bakken - de porto toevoegen en laten inkoken - de balletjes eruit halen en water toevoegen samen met de perensiroop - als alles kookt binden met de wildfond(poeder) - te serveren met gebakken witloof\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2044,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2044,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2044,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2044,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1960,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2844,\n          \"menuItemId\": 1960,\n          \"courseId\": 4988,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4988,\n            \"dispNameNl\": \"Groene pasta met pistachenoten\",\n            \"dispNameEn\": \"Green paste with pistachio nuts\",\n            \"nameNl\": \"00 groene pasta met pistachenoten,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan (geen andere pasta, anders niet meer vegan)\",\n            \"preparation\": \"pasta kook de erwten in de bouillon gedurende 15 min. en giet ze af. steam de spinazie. (hou nog een deel van de spinazie en de erwtjes opzij voor de afwerking).voeg de erwtjes, spinazie en room samen.- pureer alles tot een gladde saus.- breng op smaak met kerrie en pezo.- meng de groene saus met de pasta en voeg er ook nog enkele erwtjes en spinazie onder. crush de pistachenoten - werk af met platte peterselie en gecrushte pistachenoten.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4988,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4988,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 4988,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1961,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2845,\n          \"menuItemId\": 1961,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1962,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2847,\n          \"menuItemId\": 1962,\n          \"courseId\": 908,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 908,\n            \"dispNameNl\": \"choronsaus\",\n            \"dispNameEn\": \"choron sauce\",\n            \"nameNl\": \"choron saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"tomatino onder warme b\\u00e9arnaisesaus mengen\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 908,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 908,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2849,\n          \"menuItemId\": 1962,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2846,\n          \"menuItemId\": 1962,\n          \"courseId\": 3264,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3264,\n            \"dispNameNl\": \"Kipfilet op de grill \",\n            \"dispNameEn\": \"Grilled chicken breast \",\n            \"nameNl\": \"kipfilet op de grill 1 (kippenkruiden), dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2848,\n          \"menuItemId\": 1962,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1958,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2842,\n          \"menuItemId\": 1958,\n          \"courseId\": 274,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 274,\n            \"dispNameNl\": \"Salade New York\",\n            \"dispNameEn\": \"New York salad\",\n            \"nameNl\": \"salade new york (appel, noten, brie), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton citroensap besprenkelen over de appelstukjes\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 274,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1959,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 240,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2843,\n          \"menuItemId\": 1959,\n          \"courseId\": 159,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 159,\n            \"dispNameNl\": \"Abdijbroodje \",\n            \"dispNameEn\": \"Abbey roll \",\n            \"nameNl\": \"abdijbroodje (baguelino broodje, smeerkaas, rammenas) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 159,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 159,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_cmu.raw.json",
    "content": "{\n  \"id\": 312,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2047,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2980,\n          \"menuItemId\": 2047,\n          \"courseId\": 1091,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1091,\n            \"dispNameNl\": \"Groenteloempia\",\n            \"dispNameEn\": \"Vegetable spring roll\",\n            \"nameNl\": \"groenteloempia, dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"125g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"1 stuk per persoon\",\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1091,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1091,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1091,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1091,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1091,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1091,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2048,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2981,\n          \"menuItemId\": 2048,\n          \"courseId\": 5033,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5033,\n            \"dispNameNl\": \"Quesadilla's veganlicious\",\n            \"dispNameEn\": \"Veganlicious quesadillas\",\n            \"nameNl\": \"00 quesadilla's veganlicious, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"snack winter bak de diepgevroren quinoa-groenekoolburger op een bakplaat of in de oven.- strooi de mozzarisella over de bodem van de wraps en voeg de lente-ui toe.- voeg een laagje geroosterde ma\\u00efs toe. leg de burger in de wrap en vouw toe tot een quesadilla.- voeg de quesadilla\\\"s toe aan een warme braadpan en bak elke kant een paar minuten op een laag tot middelhoog vuur tot ze licht geroosterd zijn of je kan ze ook in een paninitoestel plaatsen.- snijd in hapklare stukken.\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5033,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5033,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5033,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5033,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2051,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2984,\n          \"menuItemId\": 2051,\n          \"courseId\": 5023,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5023,\n            \"dispNameNl\": \"Quinoasalade met bietjes\",\n            \"dispNameEn\": \"Quinoa salad with beets\",\n            \"nameNl\": \"00 quinoasalade met bietjes,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade winter bereid de quinoa volgens de instructies.- meng de rode biet doorheen de quinoa.- meng ook de boerenkool; peterselie en munt erdoorheen samen met de appelazijn, limoen en ahornsiroop. voeg als laatste de edamame boontjes & avocado toe.- rooster ondertussen de pistachenootjes in een koekenpan met wat olijfolie en strooi deze over de salade heen.-\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5023,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5023,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5023,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5023,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5023,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5023,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2052,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2985,\n          \"menuItemId\": 2052,\n          \"courseId\": 1934,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1934,\n            \"dispNameNl\": \"Bagel met rosbief, veldsla, witloof en rode biet\",\n            \"dispNameEn\": \"Roast beef, lamb\\u2019s lettuce, Belgian endive and beetroot bagel\",\n            \"nameNl\": \"bagel rosbief (veldsla, witloof, rode biet), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1934,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1934,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1934,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1934,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1934,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1934,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1934,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1934,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2053,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2986,\n          \"menuItemId\": 2053,\n          \"courseId\": 4950,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4950,\n            \"dispNameNl\": \"Meergranenbroodje met humus en wintergroenten\",\n            \"dispNameEn\": \"Multigrain roll with hummus and winter vegetables\",\n            \"nameNl\": \"00 broodje fit met humus en wintergroenten (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"135 g/275g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"broodje winter\",\n            \"price\": 3.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4950,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4950,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4950,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4950,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4950,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2054,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2987,\n          \"menuItemId\": 2054,\n          \"courseId\": 3852,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3852,\n            \"dispNameNl\": \"Honey wrap\",\n            \"dispNameEn\": \"Honey wrap\",\n            \"nameNl\": \"honey wrap, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"avocado in plakjes snijden en besprenkelen met citroensap\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3852,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3852,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 3852,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3852,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3852,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2055,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 2988,\n          \"menuItemId\": 2055,\n          \"courseId\": 4952,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4952,\n            \"dispNameNl\": \"Meergranenbroodje met camembert en wintergroenten\",\n            \"dispNameEn\": \"Multigrain roll with camembert and winter vegetables\",\n            \"nameNl\": \"00 broodje fit met camembert en wintergroenten\",\n            \"nameEn\": \"\",\n            \"weight\": \"135 g/275g\",\n            \"extra\": \"veggie\",\n            \"preparation\": null,\n            \"price\": 3.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4952,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4952,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4952,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4952,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4952,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4952,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2324,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 312,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 3361,\n          \"menuItemId\": 2324,\n          \"courseId\": 877,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 877,\n            \"dispNameNl\": \"Venkelsoep\",\n            \"dispNameEn\": \"Fennel soup\",\n            \"nameNl\": \"venkelsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 877,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 877,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 877,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 877,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 877,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_cst.raw.json",
    "content": "{\n  \"id\": 296,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1847,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 2705,\n          \"menuItemId\": 1847,\n          \"courseId\": 3624,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3624,\n            \"dispNameNl\": \"Bio-knolseldersoep\",\n            \"dispNameEn\": \"Organic celeriac soup\",\n            \"nameNl\": \"bio-knolseldersoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3624,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1848,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 2708,\n          \"menuItemId\": 1848,\n          \"courseId\": 1037,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1037,\n            \"dispNameNl\": \"Indonesische wokgroenten\",\n            \"dispNameEn\": \"Indonesian-style stir-fried vegetables\",\n            \"nameNl\": \"indonesische wokgroenten, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1037,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1037,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1037,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2707,\n          \"menuItemId\": 1848,\n          \"courseId\": 4765,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4765,\n            \"dispNameNl\": \"miehoen\",\n            \"dispNameEn\": \"mihoen\",\n            \"nameNl\": \"mie - miehoen (dunnen noedel obv rijstebloem) (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"miehoen gaar maken in de bio bouillon.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2706,\n          \"menuItemId\": 1848,\n          \"courseId\": 5172,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5172,\n            \"dispNameNl\": \"bloemkool-broccoli tots\",\n            \"dispNameEn\": \"cauliflower and broccoli tots\",\n            \"nameNl\": \"00 bloemkool-broccoli tots (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de tots af in de friteuse of in de oven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5172,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5172,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1860,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 2732,\n          \"menuItemId\": 1860,\n          \"courseId\": 1021,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1021,\n            \"dispNameNl\": \"appelmoes\",\n            \"dispNameEn\": \"apple sauce\",\n            \"nameNl\": \"appelmoes, w&z\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1021,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1021,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2731,\n          \"menuItemId\": 1860,\n          \"courseId\": 1374,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1374,\n            \"dispNameNl\": \"Gemarineerde kippenbrochette \",\n            \"dispNameEn\": \"Marinated chicken skewer \",\n            \"nameNl\": \"kippenbrochette gemarineerd, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"marineren en afbakken op de grill\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1374,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1374,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2733,\n          \"menuItemId\": 1860,\n          \"courseId\": 1921,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1921,\n            \"dispNameNl\": \"aardappelwafeltjes\",\n            \"dispNameEn\": \"potato waffles\",\n            \"nameNl\": \"aardappelwafeltjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g pp\",\n            \"extra\": null,\n            \"preparation\": \"olie verwarmen tot 170\\u00b0 en wafeltjes afbakken.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1921,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2011,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2946,\n          \"menuItemId\": 2011,\n          \"courseId\": 1425,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1425,\n            \"dispNameNl\": \"pasta met paprikaroom & waterkers\",\n            \"dispNameEn\": \"paprika cream with garden cress\",\n            \"nameNl\": \"00 paprikaroom met waterkers, zvv, dd, z & w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux afsmaken met pezo en proven\\u00e7aalse kruiden. - rode paprika gaar steamen en samen met room bij tomatensaus doen - mixen en eventueel verdunnen/aandikken serveer met een vegan pasta ( volkoren spaghetti speciality anco/ fusili volkoren bio)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2003,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2931,\n          \"menuItemId\": 2003,\n          \"courseId\": 1414,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1414,\n            \"dispNameNl\": \"Spaghetti carbonara\",\n            \"dispNameEn\": \"carbonara sauce\",\n            \"nameNl\": \"Spaghetti carbonara\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"witte saus poeder toevoegen aan kokend water en goed laten doorkoken. - culinaire room toevoegen en de kruiden. - terug laten koken en gebakken spekreepjes toevoegen. + gemalen kaas erbij serveren\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1414,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1414,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2004,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2933,\n          \"menuItemId\": 2004,\n          \"courseId\": 918,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 918,\n            \"dispNameNl\": \"Hollandse saus\",\n            \"dispNameEn\": \"Dutch sauce\",\n            \"nameNl\": \"hollandse saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"zie verpakking\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 918,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 918,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2935,\n          \"menuItemId\": 2004,\n          \"courseId\": 985,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 985,\n            \"dispNameNl\": \"kroketten\",\n            \"dispNameEn\": \"croquettes\",\n            \"nameNl\": \"kroketten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 985,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 985,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2932,\n          \"menuItemId\": 2004,\n          \"courseId\": 3361,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3361,\n            \"dispNameNl\": \"Zalmburger (global gap) op de grill\",\n            \"dispNameEn\": \"Grilled salmon burger (global gap)\",\n            \"nameNl\": \"zalmburger op de grill, global gab, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g pp\",\n            \"extra\": null,\n            \"preparation\": \"zalmburger kruiden en grillen op de grill\",\n            \"price\": 5.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3361,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3361,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3361,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3361,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 3361,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 2934,\n          \"menuItemId\": 2004,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2000,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2928,\n          \"menuItemId\": 2000,\n          \"courseId\": 5562,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5562,\n            \"dispNameNl\": \"Chicken Mexicano Bowl\",\n            \"dispNameEn\": \"chicken mexicano bowl\",\n            \"nameNl\": \"Chicken Mexicano Bowl,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen en meng met de salsa mexicana mix . - Maak de tuinerwtenspread met sjalot volgens de fiche - Bak de kip. Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: de geroosterde ma\\u00efs, veldslag, farmersla en rode bonen. Werk af met de tuinerwtenspread, gebakken ajuintjes, koriander en schijfjes jalapeno. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5562,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5562,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5562,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2001,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2929,\n          \"menuItemId\": 2001,\n          \"courseId\": 4977,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4977,\n            \"dispNameNl\": \"Groentesalade\",\n            \"dispNameEn\": \"Vegetable salad\",\n            \"nameNl\": \"00 groentesalade,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4977,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4977,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4977,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2002,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 296,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2930,\n          \"menuItemId\": 2002,\n          \"courseId\": 2071,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2071,\n            \"dispNameNl\": \"Salade met perziken en zalmsalade\",\n            \"dispNameEn\": \"Salad with peaches and salmon salad\",\n            \"nameNl\": \"salade met perziken en zalmsalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"zalm stomen en laten afkoelen (slechts max. 100g per persoon gebruiken) - zalm prakken, mengen met een kleine hoeveelheid mayonaise (voeg enkel extra mayo toe als de zalmsalade nog te droog is, laat de zalm in geen geval \\u2018zwemmen\\u2019 in de mayonaise), met geplette hardgekookte eieren en peterselie - perziken in partjes snijden - veldsla en rammenas mengen. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken volgorde voor vullen van onder naar boven: gesneden perziken (2 -3 stuks) + zalmsalade (+/- 80-100 gr) + gemengde veldsla \\u2013 rammenas\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-12_hzs.raw.json",
    "content": "{\n  \"id\": 270,\n  \"menuDate\": \"2019-12-12T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1739,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 2541,\n          \"menuItemId\": 1739,\n          \"courseId\": 3624,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3624,\n            \"dispNameNl\": \"Bio-knolseldersoep\",\n            \"dispNameEn\": \"Organic celeriac soup\",\n            \"nameNl\": \"bio-knolseldersoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3624,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1743,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 2545,\n          \"menuItemId\": 1743,\n          \"courseId\": 4978,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4978,\n            \"dispNameNl\": \"Avocado-quinoasalade\",\n            \"dispNameEn\": \"Avocado and quinoa salad\",\n            \"nameNl\": \"avocado-quinoasalade, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 gr\",\n            \"extra\": null,\n            \"preparation\": \"conceptsalade winter kook de quinoa in de groentebouillon - steam de pompoenblokjes en laat afkoelen - snij de avocado in mooie blokjes en snipper de koriander- laat de rode bonen uitlekken - vermeng de groenten, de ajuin,de mais, de rode bonen, de noten, de koriander met ma\\u00efsolie, sap van limoen en pezo - en meng dit met de quinoa. - vul de saladebox met de quinoasalade.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4978,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4978,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4978,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1742,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 2544,\n          \"menuItemId\": 1742,\n          \"courseId\": 283,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 283,\n            \"dispNameNl\": \"Salade Rhodos\",\n            \"dispNameEn\": \"Rhodes salad\",\n            \"nameNl\": \"salade rhodos (penne, pesto, geroosterde knolselder), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"geroosterde knolselder: knolselder in grove stukken snijden en roosteren in olijfolie vanaf nu pestospread gran'olivia gebruiken - sandwich spread opwerken - en pesto onder conceptsalade mengen, niet in een dressingpotje feta verkruimelen over de salade\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 283,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 283,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 283,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 283,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 283,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 283,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1741,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2543,\n          \"menuItemId\": 1741,\n          \"courseId\": 1872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1872,\n            \"dispNameNl\": \"Italiaanse worteltartaar met parmezaan\",\n            \"dispNameEn\": \"Italian carrot tartare with parmesan\",\n            \"nameNl\": \"italiaanse worteltartaar met parmezaan, dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1740,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2542,\n          \"menuItemId\": 1740,\n          \"courseId\": 3201,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3201,\n            \"dispNameNl\": \"Croissant 'bacon'\",\n            \"dispNameEn\": \"\\u2018Bacon\\u2019 croissant\",\n            \"nameNl\": \"croissant bacon (eiersalade en spek), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"1 reepje bacon per croissant\",\n            \"price\": 2.3,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3201,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3201,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3201,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1745,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2547,\n          \"menuItemId\": 1745,\n          \"courseId\": 5074,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5074,\n            \"dispNameNl\": \"Viking pumpkin\",\n            \"dispNameEn\": \"Viking pumpkin\",\n            \"nameNl\": \"00 viking pumpkin,w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"veggie\",\n            \"preparation\": \"doe de pompoenblokjes in een schaal in de oven, samen met de kruiden & een beetje olijfolie, gedurende 10 minuten op 250 \\u00b0c. - beleg het viking brood met de geitenkaas, beetje honing of mango chutney en de pompoenblokjes.- steek het broodje in de panini machine gedurende circa 3 minuten.\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5074,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5074,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5074,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5074,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1744,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 2546,\n          \"menuItemId\": 1744,\n          \"courseId\": 1105,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1105,\n            \"dispNameNl\": \"Toscaanse panini\",\n            \"dispNameEn\": \"Tuscan panini\",\n            \"nameNl\": \"toscaanse panini (mozzarella,balletjes, paprikatapenade), z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1105,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1105,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1105,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1105,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1105,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1105,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1105,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1553,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 270,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 2288,\n          \"menuItemId\": 1553,\n          \"courseId\": 3424,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3424,\n            \"dispNameNl\": \"Pasta met tomaat-basilicumsaus\",\n            \"dispNameEn\": \"Pasta with tomato and basil sauce\",\n            \"nameNl\": \"pasta tomaat basilicum saus, hzs (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3424,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3424,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3424,\n                \"allergenId\": 205\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3424,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 3424,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_cde.parsed.expected.yaml",
    "content": "campus: cde\ndate: '2019-12-19'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SOUP\n    - VEGAN\n    name:\n      en: silt celeriac soup with salicorn\n      nl: Zilt knolseldersoepje met zeekraal\n  external_id: 1814\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes:\n    - VEGAN\n    name:\n      en: pumpkin surprise\n      nl: Pumpkin surprise\n  external_id: 1815\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 1\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    - GRILL\n    name:\n      en: festive turkey burger with small fries\n      nl: Festive turkey burger met strofrietjes\n  external_id: 1816\n  multiple_prices: true\n  price: '4.60'\n  sort_order: 2\n- components:\n  - allergens:\n    - EGG\n    - FISH\n    - MUSTARD\n    attributes:\n    - FISH\n    - SALAD\n    name:\n      en: Wicca salad\n      nl: Salade Wicca\n  external_id: 1831\n  multiple_prices: true\n  price: '4.60'\n  sort_order: 11\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SALAD\n    name:\n      en: buddha bowl with scampi\n      nl: Buddha bowl met scampi's\n  external_id: 1832\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 11\n- components:\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: [ ]\n    name:\n      en: wholegrain penne\n      nl: Volkoren penne\n  - allergens:\n    - CELERY\n    - WHEAT_GLUTEN\n    attributes:\n    - VEGAN\n    name:\n      en: Mediterranean vegetable sauce\n      nl: mediterraanse groentesaus\n  external_id: 2215\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens: [ ]\n    attributes: [ ]\n    name:\n      en: farfale\n      nl: Farfale\n  - allergens:\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - PIG\n    name:\n      en: Milanese sauce with cubed ham\n      nl: milanese saus met hamblokjes\n  external_id: 2216\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n"
  },
  {
    "path": "tests/external_menus/2019-12-19_cde.processed.expected.yaml",
    "content": "campus: cde\ndate: '2019-12-19'\nmenu:\n- course_allergens:\n  - CELERY\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - SOUP\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: SOUP\n  external_id: 1814\n  name:\n    en: Silt celeriac soup with salicorn\n    nl: Zilt knolseldersoepje met zeekraal\n  price_staff: null\n  price_students: '0.90'\n- course_allergens:\n  - WHEAT_GLUTEN\n  course_attributes:\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: DAILY\n  external_id: 1815\n  name:\n    en: Pumpkin surprise\n    nl: Pumpkin surprise\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - CELERY\n  - EGG\n  - MILK_LACTOSE\n  - MUSTARD\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  - GRILL\n  course_sub_type: NORMAL\n  course_type: GRILL\n  external_id: 1816\n  name:\n    en: Festive turkey burger with small fries\n    nl: Festive turkey burger met strofrietjes\n  price_staff: '5.70'\n  price_students: '4.60'\n- course_allergens:\n  - EGG\n  - FISH\n  - MUSTARD\n  course_attributes:\n  - FISH\n  - SALAD\n  course_sub_type: NORMAL\n  course_type: SALAD\n  external_id: 1831\n  name:\n    en: Wicca salad\n    nl: Salade Wicca\n  price_staff: '5.70'\n  price_students: '4.60'\n- course_allergens:\n  - NUTS\n  - SESAME\n  - SHELLFISH\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - FISH\n  - SALAD\n  course_sub_type: NORMAL\n  course_type: SALAD\n  external_id: 1832\n  name:\n    en: Buddha bowl with scampi\n    nl: Buddha bowl met scampi's\n  price_staff: '6.20'\n  price_students: '5.00'\n- course_allergens:\n  - CELERY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: PASTA\n  external_id: 2215\n  name:\n    en: Wholegrain penne, Mediterranean vegetable sauce\n    nl: Volkoren penne, mediterraanse groentesaus\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - PIG\n  course_sub_type: NORMAL\n  course_type: PASTA\n  external_id: 2216\n  name:\n    en: Farfale, Milanese sauce with cubed ham\n    nl: Farfale, milanese saus met hamblokjes\n  price_staff: '4.70'\n  price_students: '3.80'\n"
  },
  {
    "path": "tests/external_menus/2019-12-19_cde.raw.json",
    "content": "{\n  \"id\": 291,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1814,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 3123,\n          \"menuItemId\": 1814,\n          \"courseId\": 5575,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5575,\n            \"dispNameNl\": \"Zilt knolseldersoepje met zeekraal\",\n            \"dispNameEn\": \"silt celeriac soup with salicorn\",\n            \"nameNl\": \"Zilt knolseldersoepje met zeekraal, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoof de ui aan, samen met de prei en de knolselder in boter. Bevochtig daarna met de bouillon en laat zachtjes koken tot de groenten gaar zijn. Werk af met de kervel en de zeekraal.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1815,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3124,\n          \"menuItemId\": 1815,\n          \"courseId\": 5578,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5578,\n            \"dispNameNl\": \"Pumpkin surprise\",\n            \"dispNameEn\": \"pumpkin surprise\",\n            \"nameNl\": \"Pumpkin surprise\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Verwam de oven voor op 180 graden en gebruik de grillstand. Snijd de pompoen in de breedte doormidden. Haal het vruchtvlees eruit en snijd in de breedte dunne ringen. Maak de quinoa klaar volgens de bereidingswijze op de verpakking. De groenten in gelijke hoeveelheden verdelen en laten ontdooien. Tijdens de service de groenten kort aanbakken met de garam masala en de gepelde tomaat en een beetje water toevoegen . Bouillion naar smaak  toevoegen en kokosmelk laten opkoken. Proef de curry en voeg naar smaak meer kruiden toe. Leg daarna een pompoenring op een bord, vul het binnenste met een beetje quinoa, schep vervolgens de curry erop en garneer met peterselie & een plukje waterkers.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5578,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5578,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1816,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 3125,\n          \"menuItemId\": 1816,\n          \"courseId\": 5576,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5576,\n            \"dispNameNl\": \"Festive turkey burger  met strofrietjes\",\n            \"dispNameEn\": \"festive turkey burger with small fries \",\n            \"nameNl\": \"Festive turkey burger met strofrietjes & rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2215,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3238,\n          \"menuItemId\": 2215,\n          \"courseId\": 1432,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1432,\n            \"dispNameNl\": \"mediterraanse groentesaus\",\n            \"dispNameEn\": \"Mediterranean vegetable sauce\",\n            \"nameNl\": \"mediterraanse groentesaus, zvv, dd z&w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"pastasaus groenten aanstoven en bevochtigen met water. - kruiden toevoegen en een half uurtje laten pruttelen. - afbinden opgelet: vegan pasta voorzien\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1432,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1432,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1432,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 3239,\n          \"menuItemId\": 2215,\n          \"courseId\": 5488,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5488,\n            \"dispNameNl\": \"Volkoren penne\",\n            \"dispNameEn\": \"wholegrain penne\",\n            \"nameNl\": \"Volkoren penne (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200 gr pp\",\n            \"extra\": \"\",\n            \"preparation\": \"Kook de pasta gaar in lichtgezouten water, olie toevoegen. de kooktijd is afhankelijk van de soort pasta.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5488,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2216,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 3240,\n          \"menuItemId\": 2216,\n          \"courseId\": 3502,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3502,\n            \"dispNameNl\": \"milanese saus met hamblokjes\",\n            \"dispNameEn\": \"Milanese sauce with cubed ham\",\n            \"nameNl\": \"milanese saus met hamblokjes, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"keuze in het gebruik van kappertjes 100ml of kappers 3l - tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water met bouillon toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux indien nodig (optioneel). - afsmaken met pezo en proven\\u00e7aalse kruiden. - sjalotjes aanstoven en bevochtigen met witte wijn - tomatenblokjes, kappertjes, lookpuree en kruiden toevoegen . - 15 minuten laten sudderen en bij warme tomatensaus voegen. - hamblokjes opwarmen en op einde toevoegen\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3502,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3502,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3502,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": true,\n            \"enabled\": false,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 3241,\n          \"menuItemId\": 2216,\n          \"courseId\": 5484,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5484,\n            \"dispNameNl\": \"Farfale\",\n            \"dispNameEn\": \"farfale\",\n            \"nameNl\": \"Farfale, kookvast\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200 g pp\",\n            \"extra\": \"\",\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, olie toevoegen. de kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1831,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 3129,\n          \"menuItemId\": 1831,\n          \"courseId\": 1924,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1924,\n            \"dispNameNl\": \"Salade Wicca\",\n            \"dispNameEn\": \"Wicca salad\",\n            \"nameNl\": \"salade wicca (zalm, linzen, aardappelen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"pompoenblokjes en aardappelen: niet natuur stomen, maar garen in de oven met olijfolie en rozemarijn. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1832,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 291,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 2677,\n          \"menuItemId\": 1832,\n          \"courseId\": 5560,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5560,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"buddha bowl with scampi\",\n            \"nameNl\": \"Buddha bowl met scampi's, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - Maak de scampi's klaar & laat afkoelen.- Maak de bowl: Doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en raapjes, een rijtje veldsla, schijfjes avocado (besprenkelt met citroensap) en rijtje geraspte rode bieten. Werk af met de scampi's, koriander en gesneden pijpajuin. Serveer hier bij de sojavinaigrette. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5560,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5560,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5560,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5560,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5560,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5560,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5560,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_cgb.raw.json",
    "content": "{\n  \"id\": 356,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2340,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 356,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 3374,\n          \"menuItemId\": 2340,\n          \"courseId\": 5575,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5575,\n            \"dispNameNl\": \"Zilt knolseldersoepje met zeekraal\",\n            \"dispNameEn\": \"silt celeriac soup with salicorn\",\n            \"nameNl\": \"Zilt knolseldersoepje met zeekraal, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoof de ui aan, samen met de prei en de knolselder in boter. Bevochtig daarna met de bouillon en laat zachtjes koken tot de groenten gaar zijn. Werk af met de kervel en de zeekraal.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2431,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 356,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3488,\n          \"menuItemId\": 2431,\n          \"courseId\": 5586,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5586,\n            \"dispNameNl\": \"Pasta met soja-bolognaisesaus\",\n            \"dispNameEn\": \"Pasta with soy bolognaise sauce\",\n            \"nameNl\": \"Pasta met soja-bolognaisesaus\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5586,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5586,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2341,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 356,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 3375,\n          \"menuItemId\": 2341,\n          \"courseId\": 3141,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3141,\n            \"dispNameNl\": \"Croissant met brie\",\n            \"dispNameEn\": \"Croissant with brie\",\n            \"nameNl\": \"croissant brie, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"amandelschilfers roosteren\",\n            \"price\": 2.3,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2401,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 356,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3451,\n          \"menuItemId\": 2401,\n          \"courseId\": 1890,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1890,\n            \"dispNameNl\": \"Salade zonder sla\",\n            \"dispNameEn\": \"Salad without lettuce\",\n            \"nameNl\": \"salade slaatje zonder sla (aardappel, appel, raapjes), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton appel besprenkelen met citroensap\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_cmi.raw.json",
    "content": "{\n  \"id\": 267,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2027,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 2959,\n          \"menuItemId\": 2027,\n          \"courseId\": 5575,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5575,\n            \"dispNameNl\": \"Zilt knolseldersoepje met zeekraal\",\n            \"dispNameEn\": \"silt celeriac soup with salicorn\",\n            \"nameNl\": \"Zilt knolseldersoepje met zeekraal, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoof de ui aan, samen met de prei en de knolselder in boter. Bevochtig daarna met de bouillon en laat zachtjes koken tot de groenten gaar zijn. Werk af met de kervel en de zeekraal.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2028,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 2961,\n          \"menuItemId\": 2028,\n          \"courseId\": 5578,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5578,\n            \"dispNameNl\": \"Pumpkin surprise\",\n            \"dispNameEn\": \"pumpkin surprise\",\n            \"nameNl\": \"Pumpkin surprise\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Verwam de oven voor op 180 graden en gebruik de grillstand. Snijd de pompoen in de breedte doormidden. Haal het vruchtvlees eruit en snijd in de breedte dunne ringen. Maak de quinoa klaar volgens de bereidingswijze op de verpakking. De groenten in gelijke hoeveelheden verdelen en laten ontdooien. Tijdens de service de groenten kort aanbakken met de garam masala en de gepelde tomaat en een beetje water toevoegen . Bouillion naar smaak  toevoegen en kokosmelk laten opkoken. Proef de curry en voeg naar smaak meer kruiden toe. Leg daarna een pompoenring op een bord, vul het binnenste met een beetje quinoa, schep vervolgens de curry erop en garneer met peterselie & een plukje waterkers.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5578,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5578,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2029,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 2962,\n          \"menuItemId\": 2029,\n          \"courseId\": 5576,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5576,\n            \"dispNameNl\": \"Festive turkey burger  met strofrietjes\",\n            \"dispNameEn\": \"festive turkey burger with small fries \",\n            \"nameNl\": \"Festive turkey burger met strofrietjes & rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2419,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3477,\n          \"menuItemId\": 2419,\n          \"courseId\": 5586,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5586,\n            \"dispNameNl\": \"Pasta met soja-bolognaisesaus\",\n            \"dispNameEn\": \"Pasta with soy bolognaise sauce\",\n            \"nameNl\": \"Pasta met soja-bolognaisesaus\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5586,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5586,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5586,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2181,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 3169,\n          \"menuItemId\": 2181,\n          \"courseId\": 1402,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1402,\n            \"dispNameNl\": \"Lasagne bolognaise\",\n            \"dispNameEn\": \"Lasagne bolognese\",\n            \"nameNl\": \"lasagne bolognaise kant-en klaar\",\n            \"nameEn\": \"\",\n            \"weight\": \"450g\",\n            \"extra\": null,\n            \"preparation\": \"de lasagne in porties snijden. - 20 minuten opwaren in een oven van 200\\u00b0c. - eventueel van verpakking ontdoen en in een gastronorm bak opwarmen. - geraspte kaas erbij serveren.\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1402,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1402,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1402,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1402,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1402,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2182,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 3170,\n          \"menuItemId\": 2182,\n          \"courseId\": 3141,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3141,\n            \"dispNameNl\": \"Croissant met brie\",\n            \"dispNameEn\": \"Croissant with brie\",\n            \"nameNl\": \"croissant brie, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"amandelschilfers roosteren\",\n            \"price\": 2.3,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3141,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3141,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2371,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 267,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 3403,\n          \"menuItemId\": 2371,\n          \"courseId\": 1890,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1890,\n            \"dispNameNl\": \"Salade zonder sla\",\n            \"dispNameEn\": \"Salad without lettuce\",\n            \"nameNl\": \"salade slaatje zonder sla (aardappel, appel, raapjes), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton appel besprenkelen met citroensap\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_cmu.raw.json",
    "content": "{\n  \"id\": 326,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2122,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 3070,\n          \"menuItemId\": 2122,\n          \"courseId\": 5575,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5575,\n            \"dispNameNl\": \"Zilt knolseldersoepje met zeekraal\",\n            \"dispNameEn\": \"silt celeriac soup with salicorn\",\n            \"nameNl\": \"Zilt knolseldersoepje met zeekraal, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoof de ui aan, samen met de prei en de knolselder in boter. Bevochtig daarna met de bouillon en laat zachtjes koken tot de groenten gaar zijn. Werk af met de kervel en de zeekraal.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2150,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3101,\n          \"menuItemId\": 2150,\n          \"courseId\": 5016,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5016,\n            \"dispNameNl\": \"Pompoen-falafelwrap\",\n            \"dispNameEn\": \"Pumpkin and falafel wrap\",\n            \"nameNl\": \"00 wrap pompoen-falafel,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptbroodje winter\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5016,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5016,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5016,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5016,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2151,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 3102,\n          \"menuItemId\": 2151,\n          \"courseId\": 4990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4990,\n            \"dispNameNl\": \"Kruidig roggebrood\",\n            \"dispNameEn\": \"Herbed rye bread\",\n            \"nameNl\": \"00 kruidig roggebrood, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptbroodje winter werk in 3 lagen met het roggebrood.- besmeer de 3 sneden brood met de vegane mayonaise.- leg op de eerste boterham de vegan kruidenkaas, de schijfjes raap en de waterkers.- leg de 2de boterham hierop en herhaal met het beleg en sluit met het laatste sneetje brood.- snijd de boterham diagonaal doormidden en doe er een cocktailprikker in\",\n            \"price\": 2.7,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4990,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4990,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2121,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3071,\n          \"menuItemId\": 2121,\n          \"courseId\": 4716,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4716,\n            \"dispNameNl\": \"Prinses op de erwt-broodje\",\n            \"dispNameEn\": \"Princess and the pea sandwich\",\n            \"nameNl\": \"00 prinses op de erwt broodje,z&w\",\n            \"nameEn\": \"\",\n            \"weight\": \"115 g/235 g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4716,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4716,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4716,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4716,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4716,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2323,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 3360,\n          \"menuItemId\": 2323,\n          \"courseId\": 3866,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3866,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter 6 falafels per persoon- pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox gele wortel in fijne reepjes snijden (optioneel toevoegen) hzs: falafel in oven bakken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2147,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 3098,\n          \"menuItemId\": 2147,\n          \"courseId\": 274,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 274,\n            \"dispNameNl\": \"Salade New York\",\n            \"dispNameEn\": \"New York salad\",\n            \"nameNl\": \"salade new york (appel, noten, brie), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton citroensap besprenkelen over de appelstukjes\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 274,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 274,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2148,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 3099,\n          \"menuItemId\": 2148,\n          \"courseId\": 4989,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4989,\n            \"dispNameNl\": \"Vegan burger deluxe\",\n            \"dispNameEn\": \"Deluxe vegan burger\",\n            \"nameNl\": \"00 vegan burger deluxe,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan (snack)\",\n            \"preparation\": \"snack winter bak de burgers.- bestrijk de broodjes met de salsa mexicana, de piri piri saus en ma\\u00efs, de burger en de farmersla.\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4989,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4989,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4989,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4989,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2149,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 326,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 3100,\n          \"menuItemId\": 2149,\n          \"courseId\": 5034,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5034,\n            \"dispNameNl\": \"Panini green mozzarella\",\n            \"dispNameEn\": \"Panini green mozzarella\",\n            \"nameNl\": \"00 panini green mozzarella, w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"veggie\",\n            \"preparation\": null,\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5034,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5034,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5034,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_cst.raw.json",
    "content": "{\n  \"id\": 323,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 2102,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 3066,\n          \"menuItemId\": 2102,\n          \"courseId\": 5575,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5575,\n            \"dispNameNl\": \"Zilt knolseldersoepje met zeekraal\",\n            \"dispNameEn\": \"silt celeriac soup with salicorn\",\n            \"nameNl\": \"Zilt knolseldersoepje met zeekraal, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoof de ui aan, samen met de prei en de knolselder in boter. Bevochtig daarna met de bouillon en laat zachtjes koken tot de groenten gaar zijn. Werk af met de kervel en de zeekraal.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5575,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5575,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2118,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 3068,\n          \"menuItemId\": 2118,\n          \"courseId\": 5578,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5578,\n            \"dispNameNl\": \"Pumpkin surprise\",\n            \"dispNameEn\": \"pumpkin surprise\",\n            \"nameNl\": \"Pumpkin surprise\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Verwam de oven voor op 180 graden en gebruik de grillstand. Snijd de pompoen in de breedte doormidden. Haal het vruchtvlees eruit en snijd in de breedte dunne ringen. Maak de quinoa klaar volgens de bereidingswijze op de verpakking. De groenten in gelijke hoeveelheden verdelen en laten ontdooien. Tijdens de service de groenten kort aanbakken met de garam masala en de gepelde tomaat en een beetje water toevoegen . Bouillion naar smaak  toevoegen en kokosmelk laten opkoken. Proef de curry en voeg naar smaak meer kruiden toe. Leg daarna een pompoenring op een bord, vul het binnenste met een beetje quinoa, schep vervolgens de curry erop en garneer met peterselie & een plukje waterkers.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5578,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5578,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2117,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 3067,\n          \"menuItemId\": 2117,\n          \"courseId\": 5576,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5576,\n            \"dispNameNl\": \"Festive turkey burger  met strofrietjes\",\n            \"dispNameEn\": \"festive turkey burger with small fries \",\n            \"nameNl\": \"Festive turkey burger met strofrietjes & rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5576,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5576,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2119,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 3069,\n          \"menuItemId\": 2119,\n          \"courseId\": 5037,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5037,\n            \"dispNameNl\": \"Salade Orzo \",\n            \"dispNameEn\": \"Orzo salad \",\n            \"nameNl\": \"00 salade orzo (scampi's, griekse deegwaren),w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350 gr\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scampi's: 5 stuks per conceptsalade. meer dan de helft van de salade moet bestaan uit groenten. - afwerken met postelein\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5037,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5037,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5037,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5037,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5037,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2318,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 3355,\n          \"menuItemId\": 2318,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 3354,\n          \"menuItemId\": 2318,\n          \"courseId\": 1121,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1121,\n            \"dispNameNl\": \"soja-bolognaisesaus \",\n            \"dispNameEn\": \"soy bolognese sauce \",\n            \"nameNl\": \"soja - bolognaise saus dd (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"pastasaus vege gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux . alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1121,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1121,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1121,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2319,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 323,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 3356,\n          \"menuItemId\": 2319,\n          \"courseId\": 1433,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1433,\n            \"dispNameNl\": \"zalm-broccolisaus\",\n            \"dispNameEn\": \"salmon-broccoli sauce\",\n            \"nameNl\": \"zalm broccoli saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"ajuin en witte wijn tot de helft laten inkoken en bevochtigen met water. - als het kookt witte wijn saus toevoegen en laten door koken ,daarna mixen. - broccoli stomen en in kleinere stukjes snijden de zalm ook fijner snijden . -broccoli en zalm op het laatste moment toevoegen.\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1433,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1433,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1433,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1433,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 1433,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1433,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2019-12-19_hzs.raw.json",
    "content": "{\n  \"id\": 306,\n  \"menuDate\": \"2019-12-19T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 1924,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 2798,\n          \"menuItemId\": 1924,\n          \"courseId\": 2530,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2530,\n            \"dispNameNl\": \"Bio-wortelsoep\",\n            \"dispNameEn\": \"Organic carrot soup\",\n            \"nameNl\": \"bio-wortelsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1925,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 2799,\n          \"menuItemId\": 1925,\n          \"courseId\": 161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 161,\n            \"dispNameNl\": \"New Delhi \",\n            \"dispNameEn\": \"New Delhi \",\n            \"nameNl\": \"new delhi (kip, smeerkaas, kerrie) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 161,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 161,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 161,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1926,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 2800,\n          \"menuItemId\": 1926,\n          \"courseId\": 3161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3161,\n            \"dispNameNl\": \"Bagel pumpkin\",\n            \"dispNameEn\": \"Pumpkin bagel\",\n            \"nameNl\": \"00 bagel pumpkin (cottage cheese,geroosterde pompoen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"pompoen met olijfolie en pezo roosteren in de oven. bestrijk 1 kant van de bagel met de cottage cheese, beleg met de geroosterde pompoen en werk af met gecrunchte walnoten.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1927,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 2801,\n          \"menuItemId\": 1927,\n          \"courseId\": 3143,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3143,\n            \"dispNameNl\": \"Granaatappel-fetasalade\",\n            \"dispNameEn\": \"Pomegranate and feta salad\",\n            \"nameNl\": \"granaatappel-fetasalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"laagjes van onder naar boven: feta m\\u00e9t olie (deels laten uitlekken) veldsla quinoa granaatappelpit waterkers\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3143,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3143,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3143,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3143,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3143,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3143,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1928,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 2802,\n          \"menuItemId\": 1928,\n          \"courseId\": 4981,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4981,\n            \"dispNameNl\": \"Salade met krieltjes en pompoenpitten\",\n            \"dispNameEn\": \"New potato and pumpkin seed salad\",\n            \"nameNl\": \"00 salade met krieltjes & pompoenpitten,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade,winter de krieltjes met schil snijden, inwrijven met olie en pezo en grillen in de oven.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4981,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4981,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4981,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4981,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1929,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 2803,\n          \"menuItemId\": 1929,\n          \"courseId\": 5026,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5026,\n            \"dispNameNl\": \"Pizza-pita vegan delight\",\n            \"dispNameEn\": \"Vegan delight pizza-pita\",\n            \"nameNl\": \"00 pizza pita vegan delight, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"snack winter snij het pitabroodje open, smeer langs 1 kant de tomatensaus en langs de andere kant de pesto.- beleg 1 kant met mozzarisella, de geroosterde paprika & postelein.- sluit het broodje en leg tussen de paninimachine.\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5026,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5026,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5026,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5026,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5026,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1930,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 2804,\n          \"menuItemId\": 1930,\n          \"courseId\": 4625,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4625,\n            \"dispNameNl\": \"Panini met brie en spek\",\n            \"dispNameEn\": \"Brie and bacon panini\",\n            \"nameNl\": \"00 panini brie-spek, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4625,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4625,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4625,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4625,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4625,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4625,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4625,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 1931,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 2805,\n          \"menuItemId\": 1931,\n          \"courseId\": 3422,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3422,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese\",\n            \"nameNl\": \"pasta bolognaise, hzs\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3422,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3422,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 3422,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2152,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 3103,\n          \"menuItemId\": 2152,\n          \"courseId\": 4065,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4065,\n            \"dispNameNl\": \"El triangulo\",\n            \"dispNameEn\": \"El triangulo\",\n            \"nameNl\": \"el triangulo (gerookte zalm, aardappelsla), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 4065,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4065,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4065,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 2153,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 306,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 3104,\n          \"menuItemId\": 2153,\n          \"courseId\": 1924,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1924,\n            \"dispNameNl\": \"Salade Wicca\",\n            \"dispNameEn\": \"Wicca salad\",\n            \"nameNl\": \"salade wicca (zalm, linzen, aardappelen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"pompoenblokjes en aardappelen: niet natuur stomen, maar garen in de oven met olijfolie en rozemarijn. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_cde.raw.json",
    "content": "{\n  \"id\": 544,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3751,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5234,\n          \"menuItemId\": 3751,\n          \"courseId\": 857,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 857,\n            \"dispNameNl\": \"Kervelsoep\",\n            \"dispNameEn\": \"Chervil soup\",\n            \"nameNl\": \"kervelsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 857,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 857,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 857,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3752,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5237,\n          \"menuItemId\": 3752,\n          \"courseId\": 974,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 974,\n            \"dispNameNl\": \"aardappelen met bieslook\",\n            \"dispNameEn\": \"potatoes and chives\",\n            \"nameNl\": \"aardappelen met bieslook\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"de aardappelen gaar stomen (ongeveer 20 min). - bieslook versnipperen en mengen met de aardappelen. - de margarine klaren en mengen met de aardappelen (optioneel)\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5235,\n          \"menuItemId\": 3752,\n          \"courseId\": 1273,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1273,\n            \"dispNameNl\": \"Groenteballetjes \",\n            \"dispNameEn\": \"Vegetable meatballs \",\n            \"nameNl\": \"groenteballetjes dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"3x50g\",\n            \"extra\": null,\n            \"preparation\": \"friteuse voorverwarmen op 160\\u00b0c. - balletjes kleuren en per 30 stuks op bakplaatjes schikken. - oven voorverwarmen op 180\\u00b0c - burger verhitten tot kern van minstens 65 graden bereikt is. - in bain-marie schikken. - 150 gram pp\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1273,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1273,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1273,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1273,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1273,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1273,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5236,\n          \"menuItemId\": 3752,\n          \"courseId\": 3207,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3207,\n            \"dispNameNl\": \"vergeten groenten\",\n            \"dispNameEn\": \"forgotten vegetables\",\n            \"nameNl\": \"vergeten groenten, z & w \",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3753,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5240,\n          \"menuItemId\": 3753,\n          \"courseId\": 974,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 974,\n            \"dispNameNl\": \"aardappelen met bieslook\",\n            \"dispNameEn\": \"potatoes and chives\",\n            \"nameNl\": \"aardappelen met bieslook\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"de aardappelen gaar stomen (ongeveer 20 min). - bieslook versnipperen en mengen met de aardappelen. - de margarine klaren en mengen met de aardappelen (optioneel)\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5238,\n          \"menuItemId\": 3753,\n          \"courseId\": 1395,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1395,\n            \"dispNameNl\": \"Vogelnestje\",\n            \"dispNameEn\": \"Scotch egg\",\n            \"nameNl\": \"vogelnestje\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"vogelnestjes schikken op een ingeboterde lage gastro. - de vogelnestjes bestrijken met geklaarde boter afkruiden. - garen in de oven op 175 graden gedurende ongeveer 12 min tot een temperatuur van minstens 65\\u00b0c bereikt is\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1395,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1395,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1395,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1395,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5239,\n          \"menuItemId\": 3753,\n          \"courseId\": 3207,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3207,\n            \"dispNameNl\": \"vergeten groenten\",\n            \"dispNameEn\": \"forgotten vegetables\",\n            \"nameNl\": \"vergeten groenten, z & w \",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4070,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5648,\n          \"menuItemId\": 4070,\n          \"courseId\": 1425,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1425,\n            \"dispNameNl\": \"pasta met paprikaroom & waterkers\",\n            \"dispNameEn\": \"paprika cream with garden cress\",\n            \"nameNl\": \"00 paprikaroom met waterkers, zvv, dd, z & w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux afsmaken met pezo en proven\\u00e7aalse kruiden. - rode paprika gaar steamen en samen met room bij tomatensaus doen - mixen en eventueel verdunnen/aandikken serveer met een vegan pasta ( volkoren spaghetti speciality anco/ fusili volkoren bio)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4075,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5654,\n          \"menuItemId\": 4075,\n          \"courseId\": 925,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 925,\n            \"dispNameNl\": \"Milanese saus met hamblokjes\",\n            \"dispNameEn\": \"Milanese sauce with cubed ham\",\n            \"nameNl\": \"milanese saus met hamblokjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"keuze in het gebruik van kappertjes 100ml of kappers 3l - tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water met bouillon toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux indien nodig (optioneel). - afsmaken met pezo en proven\\u00e7aalse kruiden. - sjalotjes aanstoven en bevochtigen met witte wijn - tomatenblokjes, kappertjes, lookpuree en kruiden toevoegen . - 15 minuten laten sudderen en bij warme tomatensaus voegen. - hamblokjes opwarmen en op einde toevoegen\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 925,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 925,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 925,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5655,\n          \"menuItemId\": 4075,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4079,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5664,\n          \"menuItemId\": 4079,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5665,\n          \"menuItemId\": 4079,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5663,\n          \"menuItemId\": 4079,\n          \"courseId\": 5525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5525,\n            \"dispNameNl\": \"Scampi's\",\n            \"dispNameEn\": \"scampi\",\n            \"nameNl\": \"Scampi's\",\n            \"nameEn\": \"\",\n            \"weight\": \"6 - 7 stuks\",\n            \"extra\": \"\",\n            \"preparation\": \"insmeren met mengeling van saus lemon/green peper en soja olie - grillen\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5525,\n                \"allergenId\": 207\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5525,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 5525,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3886,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5412,\n          \"menuItemId\": 3886,\n          \"courseId\": 1892,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1892,\n            \"dispNameNl\": \"Boerensalade\",\n            \"dispNameEn\": \"Farmer\\u2019s salad\",\n            \"nameNl\": \"boerensalade (rosbief, aardappelsalade), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"rosbief: 2 sneetjes van 25g\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1892,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1892,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1892,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1892,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1892,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1892,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1892,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4024,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 544,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5595,\n          \"menuItemId\": 4024,\n          \"courseId\": 5531,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5531,\n            \"dispNameNl\": \"Sweet potato bowl\",\n            \"dispNameEn\": \"Sweet potato bowl\",\n            \"nameNl\": \"Sweet potato bowl, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de quinoa klaar volgens de bereidingswijze van de fiche, laat de quinoa afkoelen.  Maak de zoete aardappel klaar volgens de fiche en breng op smaak met kaneel. Maak de bowl: doe de quinoa in een kommetje, vervolgens de bonen, zoete aardappel, curly kale & raapjes.  Werk af met kipreepjes, cashewnoten, platte peterselie & furikake. Serveer hierbij de vinaigrette.Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_cgb.raw.json",
    "content": "{\n  \"id\": 631,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3910,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 631,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5440,\n          \"menuItemId\": 3910,\n          \"courseId\": 1605,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1605,\n            \"dispNameNl\": \"Roomsoep van schorseneren\",\n            \"dispNameEn\": \"Cream of salsify\",\n            \"nameNl\": \"roomsoep van schorseneren\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1605,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 1605,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3911,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 631,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5441,\n          \"menuItemId\": 3911,\n          \"courseId\": 4975,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4975,\n            \"dispNameNl\": \"Salade Marrakech veganlicious \",\n            \"dispNameEn\": \"Marrakesh veganlicious salad \",\n            \"nameNl\": \"00 salade marrakech veganlicious(couscous,mozzarisella),dd,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 gr\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3916,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 631,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5446,\n          \"menuItemId\": 3916,\n          \"courseId\": 3161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3161,\n            \"dispNameNl\": \"Bagel pumpkin\",\n            \"dispNameEn\": \"Pumpkin bagel\",\n            \"nameNl\": \"00 bagel pumpkin (cottage cheese,geroosterde pompoen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"pompoen met olijfolie en pezo roosteren in de oven. bestrijk 1 kant van de bagel met de cottage cheese, beleg met de geroosterde pompoen en werk af met gecrunchte walnoten.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3921,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 631,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5451,\n          \"menuItemId\": 3921,\n          \"courseId\": 2424,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2424,\n            \"dispNameNl\": \"Panini 'kalkoen-pesto'\",\n            \"dispNameEn\": \"Turkey pesto panini\",\n            \"nameNl\": \"panini 'kalkoen-pesto', z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": null,\n            \"preparation\": \"vanaf nu pestospread gran'olivia gebruiken - sandwich spread opwerken\",\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_cmi.raw.json",
    "content": "{\n  \"id\": 534,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3312,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4593,\n          \"menuItemId\": 3312,\n          \"courseId\": 1605,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1605,\n            \"dispNameNl\": \"Roomsoep van schorseneren\",\n            \"dispNameEn\": \"Cream of salsify\",\n            \"nameNl\": \"roomsoep van schorseneren\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1605,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1605,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 1605,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3311,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4592,\n          \"menuItemId\": 3311,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4589,\n          \"menuItemId\": 3311,\n          \"courseId\": 1269,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1269,\n            \"dispNameNl\": \"Falafel\",\n            \"dispNameEn\": \"Falafel\",\n            \"nameNl\": \"falafel dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"6x14g\",\n            \"extra\": null,\n            \"preparation\": \"frituur voorverwarmen op 170\\u00b0. - falafel afbakken tot een temperatuur van minstens 65\\u00b0c bereikt is . - in bain marie schikken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1269,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1269,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5506,\n          \"menuItemId\": 3311,\n          \"courseId\": 5514,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5514,\n            \"dispNameNl\": \"Rauwkostslaatje\",\n            \"dispNameEn\": \"crudit\\u00e9s\",\n            \"nameNl\": \"rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is een slaatje om bij een gerecht toe te voegen, waarbij de salade al in de prijs gerekend is.\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3313,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4595,\n          \"menuItemId\": 3313,\n          \"courseId\": 903,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 903,\n            \"dispNameNl\": \"barbecuesaus\",\n            \"dispNameEn\": \"BBQ sauce\",\n            \"nameNl\": \"bbq saus dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"water aan de kook brengen - poeder oplossen in beetje water en toevoegen tot juiste dikte. - even laten inkoken samen met de ketchup en bbq saus. -gestoofde sjalotjes toevoegen; - keuze om ketchup 3 liter of 1 liter te gebruiken\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 903,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 903,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 903,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4597,\n          \"menuItemId\": 3313,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4594,\n          \"menuItemId\": 3313,\n          \"courseId\": 1393,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1393,\n            \"dispNameNl\": \"Varkenslapje\",\n            \"dispNameEn\": \"Pork escalope\",\n            \"nameNl\": \"varkenslapje dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": \"voorbakken in braadpan - kruiden. - afbakken in oven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1393,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4596,\n          \"menuItemId\": 3313,\n          \"courseId\": 5514,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5514,\n            \"dispNameNl\": \"Rauwkostslaatje\",\n            \"dispNameEn\": \"crudit\\u00e9s\",\n            \"nameNl\": \"rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is een slaatje om bij een gerecht toe te voegen, waarbij de salade al in de prijs gerekend is.\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4102,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5705,\n          \"menuItemId\": 4102,\n          \"courseId\": 3423,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3423,\n            \"dispNameNl\": \"Pasta met vegetarische bolognaise\",\n            \"dispNameEn\": \"Pasta with vegetarian bolognese\",\n            \"nameNl\": \"pasta vegetarische bolognaise, hzs (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3423,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4097,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5700,\n          \"menuItemId\": 4097,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4093,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5686,\n          \"menuItemId\": 4093,\n          \"courseId\": 927,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 927,\n            \"dispNameNl\": \"pepersaus\",\n            \"dispNameEn\": \"pepper sauce\",\n            \"nameNl\": \"pepersaus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"bruine saus maken (water, saus espagnole, proven\\u00e7aalse kruiden) 12 liter maken voor 100 personen. - peperbollen, room en whiskey toevoegen.\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 927,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 927,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 927,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5688,\n          \"menuItemId\": 4093,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5685,\n          \"menuItemId\": 4093,\n          \"courseId\": 1388,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1388,\n            \"dispNameNl\": \"Steak\",\n            \"dispNameEn\": \"Steak\",\n            \"nameNl\": \"steak\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"steak grillen in olie of margarine\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1388,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 1388,\n                \"courseLogoId\": 208\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5687,\n          \"menuItemId\": 4093,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4083,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5675,\n          \"menuItemId\": 4083,\n          \"courseId\": 4975,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4975,\n            \"dispNameNl\": \"Salade Marrakech veganlicious \",\n            \"dispNameEn\": \"Marrakesh veganlicious salad \",\n            \"nameNl\": \"00 salade marrakech veganlicious(couscous,mozzarisella),dd,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 gr\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4088,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 534,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5680,\n          \"menuItemId\": 4088,\n          \"courseId\": 3161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3161,\n            \"dispNameNl\": \"Bagel pumpkin\",\n            \"dispNameEn\": \"Pumpkin bagel\",\n            \"nameNl\": \"00 bagel pumpkin (cottage cheese,geroosterde pompoen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"pompoen met olijfolie en pezo roosteren in de oven. bestrijk 1 kant van de bagel met de cottage cheese, beleg met de geroosterde pompoen en werk af met gecrunchte walnoten.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_cmu.parsed.expected.yaml",
    "content": "$test_case:\n  course_of_interest: 3155\n  reason: |\n    This response contains a menu item which on its own has \"enabled\" set to 1, but has only one component which is has\n    \"deleted\" set to true and \"enabled\" set to false.\n\n    The official site displays these courses and as such Komidabot should as well, even if it sounds counterintuitive.\n  old_reason: |\n    This response contains a menu item which on its own has \"enabled\" set to 1, but has only one component which is has\n    \"deleted\" set to true and \"enabled\" set to false.\n\n    The course of interest here does not appear in this file as it should be ignored.\ncampus: cmu\ndate: '2020-02-10'\nmenu:\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    attributes:\n    - BIO\n    - SOUP\n    - VEGGIE\n    name:\n      en: Organic pumpkin soup\n      nl: Bio-pompoensoep\n  external_id: 3151\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - CELERY\n    - SOY\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Thai Bombai salad\n      nl: Thai bombai salade\n  external_id: 3152\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 1\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    - NUTS\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SALAD\n    - VEGGIE\n    name:\n      en: Spiced bread and goat cheese\n      nl: Peperkoeken geitenkaasje\n  external_id: 3153\n  multiple_prices: true\n  price: '4.40'\n  sort_order: 2\n- components:\n  - allergens:\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: Deluxe vegan burger\n      nl: Vegan burger deluxe\n  external_id: 3154\n  multiple_prices: false\n  price: '3.10'\n  sort_order: 7\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - NUTS\n    attributes:\n    - CHEESE\n    - SNACK\n    name:\n      en: Brie grilled cheese sandwich\n      nl: Croque brie\n  external_id: 3155\n  multiple_prices: false\n  price: '2.00'\n  sort_order: 8\n- components:\n  - allergens:\n    - PEANUTS\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: lentilicious\n      nl: Lentilicious\n  external_id: 3156\n  multiple_prices: false\n  price: '3.40'\n  sort_order: 3\n- components:\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SNACK\n    name:\n      en: Multigrain roll with MSC tuna salad\n      nl: Meergranenbroodje met MSC-tonijnsalade\n  external_id: 3157\n  multiple_prices: false\n  price: '3.40'\n  sort_order: 5\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - MILK_LACTOSE\n    - MUSTARD\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEAL\n    name:\n      en: Celery and roast beef focaccia\n      nl: Focaccia selder-rosbief\n  external_id: 3158\n  multiple_prices: false\n  price: '3.40'\n  sort_order: 4\n- components:\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - SESAME\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SNACK\n    - VEGGIE\n    name:\n      en: Multigrain roll with cheese and winter vegetables\n      nl: Meergranenbroodje met kaas en wintergroenten\n  external_id: 3159\n  multiple_prices: false\n  price: '3.00'\n  sort_order: 6\n"
  },
  {
    "path": "tests/external_menus/2020-02-10_cmu.raw.json",
    "content": "{\n  \"id\": 510,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3151,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4378,\n          \"menuItemId\": 3151,\n          \"courseId\": 5011,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5011,\n            \"dispNameNl\": \"Bio-pompoensoep\",\n            \"dispNameEn\": \"Organic pumpkin soup\",\n            \"nameNl\": \"00 bio-pompoensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml/700 ml\",\n            \"extra\": null,\n            \"preparation\": \"groenten (ajuin,prei, pompoen en aardappelen) samen met bouillon beetgaar koken. - mixen en room toevoegen. op smaak brengen met peper en zout.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5011,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3152,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4379,\n          \"menuItemId\": 3152,\n          \"courseId\": 3490,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3490,\n            \"dispNameNl\": \"Thai bombai salade\",\n            \"dispNameEn\": \"Thai Bombai salad\",\n            \"nameNl\": \"thai bombai salade, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"conceptsalade winter miehoen koken - mie mengen met de world grill saus - pastinaak grillen en mengen met de wortelen en sojascheuten en kokosschilfers - opbouw: miehoen, groentjes en platte peterselie\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3490,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3490,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3490,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3490,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3153,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4380,\n          \"menuItemId\": 3153,\n          \"courseId\": 3874,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3874,\n            \"dispNameNl\": \"Peperkoeken geitenkaasje\",\n            \"dispNameEn\": \"Spiced bread and goat cheese\",\n            \"nameNl\": \"peperkoeken geitenkaasje, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"citroensap voor de bio-appel - peperkoek in reepjes snijden en bovenaan in de saladebox\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3156,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 4383,\n          \"menuItemId\": 3156,\n          \"courseId\": 5523,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5523,\n            \"dispNameNl\": \"Lentilicious\",\n            \"dispNameEn\": \"lentilicious\",\n            \"nameNl\": \"Lentilicious\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5523,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5523,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5523,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5523,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5523,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3158,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 4385,\n          \"menuItemId\": 3158,\n          \"courseId\": 4627,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4627,\n            \"dispNameNl\": \"Focaccia selder-rosbief\",\n            \"dispNameEn\": \"Celery and roast beef focaccia\",\n            \"nameNl\": \"00 focaccia selder-rosbief, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4627,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 4627,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3157,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 4384,\n          \"menuItemId\": 3157,\n          \"courseId\": 4966,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4966,\n            \"dispNameNl\": \"Meergranenbroodje met MSC-tonijnsalade\",\n            \"dispNameEn\": \"Multigrain roll with MSC tuna salad\",\n            \"nameNl\": \"00 broodje fit msc-tonijnsalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"160 g/300 g\",\n            \"extra\": \"msc\",\n            \"preparation\": null,\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4966,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4966,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4966,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3159,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 4386,\n          \"menuItemId\": 3159,\n          \"courseId\": 72,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 72,\n            \"dispNameNl\": \"Meergranenbroodje met kaas en wintergroenten\",\n            \"dispNameEn\": \"Multigrain roll with cheese and winter vegetables\",\n            \"nameNl\": \"broodje fit met kaas en wintergroentjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g / 300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 72,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 72,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 72,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 72,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 72,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 72,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 72,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 72,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 72,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3154,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 4381,\n          \"menuItemId\": 3154,\n          \"courseId\": 4989,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4989,\n            \"dispNameNl\": \"Vegan burger deluxe\",\n            \"dispNameEn\": \"Deluxe vegan burger\",\n            \"nameNl\": \"00 vegan burger deluxe,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan (snack)\",\n            \"preparation\": \"snack winter bak de burgers.- bestrijk de broodjes met de salsa mexicana, de piri piri saus en ma\\u00efs, de burger en de farmersla.\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4989,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4989,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4989,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4989,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3155,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 510,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 4382,\n          \"menuItemId\": 3155,\n          \"courseId\": 5027,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5027,\n            \"dispNameNl\": \"Croque brie\",\n            \"dispNameEn\": \"Brie grilled cheese sandwich\",\n            \"nameNl\": \"00 croque brie, z&w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"veggie\",\n            \"preparation\": \"1 potje saus is inbegrepen in de prijs\",\n            \"price\": 2.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5027,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5027,\n                \"allergenId\": 205\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5027,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5027,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": true,\n            \"enabled\": false,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_cst.raw.json",
    "content": "{\n  \"id\": 626,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3839,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5343,\n          \"menuItemId\": 3839,\n          \"courseId\": 852,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 852,\n            \"dispNameNl\": \"Groene seldersoep\",\n            \"dispNameEn\": \"Green celery soup\",\n            \"nameNl\": \"groene seldersoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"water aan de kook brengen.groenten bouillon peper zout toevoegen. - als groenten gaar zijn mixen afbinden met roux en verder afsmaken.+ 5 kg selder afsteamen en toevoegen als garnituur op laatste moment toevoegen.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 852,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 852,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 852,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 852,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3840,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5345,\n          \"menuItemId\": 3840,\n          \"courseId\": 1010,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1010,\n            \"dispNameNl\": \"puree met wortel en pijpajuin\",\n            \"dispNameEn\": \"mashed potatoes with carrots and spring onions\",\n            \"nameNl\": \"puree met wortel en pijpajuin (zelfbereid), z\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"\\t\\t\\r\\n\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1010,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1010,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5344,\n          \"menuItemId\": 3840,\n          \"courseId\": 2741,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2741,\n            \"dispNameNl\": \"Quornworst\",\n            \"dispNameEn\": \"Quorn sausage\",\n            \"nameNl\": \"quornworst, dd java code fout 40353257\",\n            \"nameEn\": \"\",\n            \"weight\": \"120g (2x60g)\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"worsten laten ontdooien. - pan verwarmen, boter toevoegen. - worsten kleuren en in gastro schikken. - voor service afbakken in oven op 180\\u00b0\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2741,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2741,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3841,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5347,\n          \"menuItemId\": 3841,\n          \"courseId\": 1010,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1010,\n            \"dispNameNl\": \"puree met wortel en pijpajuin\",\n            \"dispNameEn\": \"mashed potatoes with carrots and spring onions\",\n            \"nameNl\": \"puree met wortel en pijpajuin (zelfbereid), z\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"\\t\\t\\r\\n\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1010,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1010,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5346,\n          \"menuItemId\": 3841,\n          \"courseId\": 1073,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1073,\n            \"dispNameNl\": \"Barbecueworst\",\n            \"dispNameEn\": \"Barbecue sausage\",\n            \"nameNl\": \"bbq worst\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g pp\",\n            \"extra\": null,\n            \"preparation\": \"marineren en grillen\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1073,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1073,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1073,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 1073,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3874,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5388,\n          \"menuItemId\": 3874,\n          \"courseId\": 930,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 930,\n            \"dispNameNl\": \"portosaus\",\n            \"dispNameEn\": \"port sauce\",\n            \"nameNl\": \"portosaus dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"water aan de kook brengen - poeder oplossen in beetje water en toevoegen tot juiste dikte. - even laten inkoken 3 kg ajuin eminceren en aanstoven in margarine - bij saus voegen. - afwerken met peterselie\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 930,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5389,\n          \"menuItemId\": 3874,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5387,\n          \"menuItemId\": 3874,\n          \"courseId\": 1388,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1388,\n            \"dispNameNl\": \"Steak\",\n            \"dispNameEn\": \"Steak\",\n            \"nameNl\": \"steak\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"steak grillen in olie of margarine\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1388,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 1388,\n                \"courseLogoId\": 208\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3884,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5410,\n          \"menuItemId\": 3884,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5409,\n          \"menuItemId\": 3884,\n          \"courseId\": 1416,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1416,\n            \"dispNameNl\": \"African sunshinesaus\",\n            \"dispNameEn\": \"African sunshine sauce\",\n            \"nameNl\": \"african sunshine saus, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"courgettes en paprika aanstoven en kruiden. - mengen met de pasta en de african sunshine saus toevoegen.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3865,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5379,\n          \"menuItemId\": 3865,\n          \"courseId\": 5563,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5563,\n            \"dispNameNl\": \"Falafelbowl\",\n            \"dispNameEn\": \"falafel bowl\",\n            \"nameNl\": \"Falafelbowl, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5563,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5563,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3875,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5606,\n          \"menuItemId\": 3875,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3908,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 5438,\n          \"menuItemId\": 3908,\n          \"courseId\": 5561,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5561,\n            \"dispNameNl\": \"Buddha bowl met zalm\",\n            \"dispNameEn\": \"buddha bowl with salmon\",\n            \"nameNl\": \"Buddha bowl met zalm,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - Maak de wasabidressing: meng de yoghurtdressing met de wasabi - Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: rijtje edamame bonen, sojascheuten, rijtje zalm en rijtje ma\\u00efs. Werk de budha bowl af met de zeewiersalade, koriander en zwart sesamzaad en tuinerwtenspread  met sjalot. Serveer hierbij de wasabimayonaise. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3864,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 626,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 5378,\n          \"menuItemId\": 3864,\n          \"courseId\": 3487,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3487,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - pastinaak grillen en mengen met de wortelen en sojascheuten en kokosschilfers - opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-10_hzs.raw.json",
    "content": "{\n  \"id\": 500,\n  \"menuDate\": \"2020-02-10T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3036,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4227,\n          \"menuItemId\": 3036,\n          \"courseId\": 856,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 856,\n            \"dispNameNl\": \"Juliennesoep\",\n            \"dispNameEn\": \"Julienne soup\",\n            \"nameNl\": \"juliennesoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"juliennegroenten samen met bouillon beetgaar koken. - op smaak brengen met peper en zout. peterselie fijn hakken en op het laatste moment toevoegen.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 856,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 856,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 856,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3041,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4232,\n          \"menuItemId\": 3041,\n          \"courseId\": 4629,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4629,\n            \"dispNameNl\": \"Bagel tzatziki\",\n            \"dispNameEn\": \"Tzatziki bagel\",\n            \"nameNl\": \"00 bagel tzatziki, z&w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"veggie\",\n            \"preparation\": null,\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4629,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4629,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3046,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4237,\n          \"menuItemId\": 3046,\n          \"courseId\": 4168,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4168,\n            \"dispNameNl\": \"Broodje met cottage cheese en serranoham\",\n            \"dispNameEn\": \"Sandwich with cottage cheese and Serrano ham\",\n            \"nameNl\": \"broodje cottage cheese-serrano, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3221,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 4450,\n          \"menuItemId\": 3221,\n          \"courseId\": 3421,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3421,\n            \"dispNameNl\": \"Pasta all' arrabiata\",\n            \"dispNameEn\": \"Pasta all'arrabbiata\",\n            \"nameNl\": \"pasta arrabiata, hzs (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3421,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3421,\n                \"allergenId\": 205\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3421,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 3421,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3051,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 4242,\n          \"menuItemId\": 3051,\n          \"courseId\": 4976,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4976,\n            \"dispNameNl\": \"Mexicaanse salade\",\n            \"dispNameEn\": \"Mexican salad\",\n            \"nameNl\": \"00 mexicaanse salade,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade winter ma\\u00efs stomen.- de avocado in kleine blokjes/schijfjes snijden.- limoenen persen (een deel over de avocado doen voor verkleuring tegen te gaan)- quinoa koken (zie fiche).- olijfolie, rode bonen, wortelstaafjes, ma\\u00efs en avocado onder de quinoa mengen.- op smaak brengen met limoensap, komijn, look, peper en zout.- afwerken met vers gehakte koriander.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4976,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4976,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4976,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3056,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 4247,\n          \"menuItemId\": 3056,\n          \"courseId\": 2071,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2071,\n            \"dispNameNl\": \"Salade met perziken en zalmsalade\",\n            \"dispNameEn\": \"Salad with peaches and salmon salad\",\n            \"nameNl\": \"salade met perziken en zalmsalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"zalm stomen en laten afkoelen (slechts max. 100g per persoon gebruiken) - zalm prakken, mengen met een kleine hoeveelheid mayonaise (voeg enkel extra mayo toe als de zalmsalade nog te droog is, laat de zalm in geen geval \\u2018zwemmen\\u2019 in de mayonaise), met geplette hardgekookte eieren en peterselie - perziken in partjes snijden - veldsla en rammenas mengen. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken volgorde voor vullen van onder naar boven: gesneden perziken (2 -3 stuks) + zalmsalade (+/- 80-100 gr) + gemengde veldsla \\u2013 rammenas\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3061,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 4252,\n          \"menuItemId\": 3061,\n          \"courseId\": 5309,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5309,\n            \"dispNameNl\": \"Groentepizza\",\n            \"dispNameEn\": \"Vegetable pizza\",\n            \"nameNl\": \"00 groentepizza,dd,w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"pizzabodems openleggen en op een ingevette gastronorm plaatsen - tomatensaus over pizzabodems verdelen - courgette schijfjes grillen en op de pizza's schikken - pizza bakken in de oven - achteraf grana padano schilfers erover strooien en de curly kale en afkruiden met toegestane allergeenvrije kruiden. pizzabodem in 8 gelijke stukken snijden.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5309,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5309,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5309,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5309,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3066,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 500,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 4257,\n          \"menuItemId\": 3066,\n          \"courseId\": 1093,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1093,\n            \"dispNameNl\": \"Pizzabaguette ham-kaas\",\n            \"dispNameEn\": \"Ham and cheese pizza baguette\",\n            \"nameNl\": \"pizza baguette ham/kaas, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1093,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1093,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_cde.raw.json",
    "content": "{\n  \"id\": 614,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 4375,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 6052,\n          \"menuItemId\": 4375,\n          \"courseId\": 3624,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3624,\n            \"dispNameNl\": \"Bio-knolseldersoep\",\n            \"dispNameEn\": \"Organic celeriac soup\",\n            \"nameNl\": \"bio-knolseldersoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3624,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 3624,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4292,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5927,\n          \"menuItemId\": 4292,\n          \"courseId\": 5046,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5046,\n            \"dispNameNl\": \"Pompoenrisotto met een boschampignonkroket\",\n            \"dispNameEn\": \"Pumpkin risotto with a forest mushroom croquette\",\n            \"nameNl\": \"00 pompoenrisotto met boschampignonkroket,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400 g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"pompoenrisotto: meng de rijst, bouillon en pompoen in een gastronorm - plaats 40 min in een oven op 200gr afgedekt - haal uit de oven en meng met pezo, olijfolie en parmesan tot een glanzend mengsel. frituur de kroketten. werk de pompoenrisotto af met kervel of postelein en 1 boschampignonkroket (100gr)\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5046,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5046,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5046,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5046,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4033,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5605,\n          \"menuItemId\": 4033,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5603,\n          \"menuItemId\": 4033,\n          \"courseId\": 2047,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2047,\n            \"dispNameNl\": \"Pita gyros\",\n            \"dispNameEn\": \"Pita gyros\",\n            \"nameNl\": \"pita gyros (+ aardappelbereiding en cocktail of looksaus) , w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"gyros aanbakken in soja olie - salade maken met wortel, rode kool, veldsla en dressing maison - keuze uit aardappelbereidingen: frietjes, wedges garlic/herb of aardappelschijfjes - cocktailsaus of looksaus\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2047,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2047,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5604,\n          \"menuItemId\": 4033,\n          \"courseId\": 5514,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5514,\n            \"dispNameNl\": \"Rauwkostslaatje\",\n            \"dispNameEn\": \"crudit\\u00e9s\",\n            \"nameNl\": \"rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is een slaatje om bij een gerecht toe te voegen, waarbij de salade al in de prijs gerekend is.\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4073,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5651,\n          \"menuItemId\": 4073,\n          \"courseId\": 1425,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1425,\n            \"dispNameNl\": \"pasta met paprikaroom & waterkers\",\n            \"dispNameEn\": \"paprika cream with garden cress\",\n            \"nameNl\": \"00 paprikaroom met waterkers, zvv, dd, z & w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux afsmaken met pezo en proven\\u00e7aalse kruiden. - rode paprika gaar steamen en samen met room bij tomatensaus doen - mixen en eventueel verdunnen/aandikken serveer met een vegan pasta ( volkoren spaghetti speciality anco/ fusili volkoren bio)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4078,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5660,\n          \"menuItemId\": 4078,\n          \"courseId\": 925,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 925,\n            \"dispNameNl\": \"Milanese saus met hamblokjes\",\n            \"dispNameEn\": \"Milanese sauce with cubed ham\",\n            \"nameNl\": \"milanese saus met hamblokjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"keuze in het gebruik van kappertjes 100ml of kappers 3l - tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water met bouillon toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux indien nodig (optioneel). - afsmaken met pezo en proven\\u00e7aalse kruiden. - sjalotjes aanstoven en bevochtigen met witte wijn - tomatenblokjes, kappertjes, lookpuree en kruiden toevoegen . - 15 minuten laten sudderen en bij warme tomatensaus voegen. - hamblokjes opwarmen en op einde toevoegen\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 925,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 925,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 925,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5661,\n          \"menuItemId\": 4078,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4082,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5674,\n          \"menuItemId\": 4082,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5673,\n          \"menuItemId\": 4082,\n          \"courseId\": 5515,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5515,\n            \"dispNameNl\": \"Saladbar\",\n            \"dispNameEn\": \"saladbar\",\n            \"nameNl\": \"Salade - bar\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is de saladbar om toe te voegen aan een grill-gerecht, is reeds ingecalculeerd in de prijs van het grill-gerecht\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5515,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5672,\n          \"menuItemId\": 4082,\n          \"courseId\": 5525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5525,\n            \"dispNameNl\": \"Scampi's\",\n            \"dispNameEn\": \"scampi\",\n            \"nameNl\": \"Scampi's\",\n            \"nameEn\": \"\",\n            \"weight\": \"6 - 7 stuks\",\n            \"extra\": \"\",\n            \"preparation\": \"insmeren met mengeling van saus lemon/green peper en soja olie - grillen\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5525,\n                \"allergenId\": 207\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5525,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 5525,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3895,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5421,\n          \"menuItemId\": 3895,\n          \"courseId\": 4188,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4188,\n            \"dispNameNl\": \"Salade met haring en appel\",\n            \"dispNameEn\": \"Apple and herring salad\",\n            \"nameNl\": \"salade haring-appel, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"onderaan in de pot, aardappelsla, hier op de haring in stukken, meng appel en witloof met zure room en gehakte perterselie en beetje citroensap. bestrooi met rode besjes en werk af met veldsla\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 4188,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4188,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4188,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4027,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 614,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 5598,\n          \"menuItemId\": 4027,\n          \"courseId\": 5531,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5531,\n            \"dispNameNl\": \"Sweet potato bowl\",\n            \"dispNameEn\": \"Sweet potato bowl\",\n            \"nameNl\": \"Sweet potato bowl, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de quinoa klaar volgens de bereidingswijze van de fiche, laat de quinoa afkoelen.  Maak de zoete aardappel klaar volgens de fiche en breng op smaak met kaneel. Maak de bowl: doe de quinoa in een kommetje, vervolgens de bonen, zoete aardappel, curly kale & raapjes.  Werk af met kipreepjes, cashewnoten, platte peterselie & furikake. Serveer hierbij de vinaigrette.Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5531,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5531,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_cgb.raw.json",
    "content": "{\n  \"id\": 634,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3931,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5463,\n          \"menuItemId\": 3931,\n          \"courseId\": 2530,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2530,\n            \"dispNameNl\": \"Bio-wortelsoep\",\n            \"dispNameEn\": \"Organic carrot soup\",\n            \"nameNl\": \"bio-wortelsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3933,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5466,\n          \"menuItemId\": 3933,\n          \"courseId\": 5561,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5561,\n            \"dispNameNl\": \"Buddha bowl met zalm\",\n            \"dispNameEn\": \"buddha bowl with salmon\",\n            \"nameNl\": \"Buddha bowl met zalm,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - Maak de wasabidressing: meng de yoghurtdressing met de wasabi - Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: rijtje edamame bonen, sojascheuten, rijtje zalm en rijtje ma\\u00efs. Werk de budha bowl af met de zeewiersalade, koriander en zwart sesamzaad en tuinerwtenspread  met sjalot. Serveer hierbij de wasabimayonaise. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3936,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5470,\n          \"menuItemId\": 3936,\n          \"courseId\": 3423,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3423,\n            \"dispNameNl\": \"Pasta met vegetarische bolognaise\",\n            \"dispNameEn\": \"Pasta with vegetarian bolognese\",\n            \"nameNl\": \"pasta vegetarische bolognaise, hzs (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3423,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3924,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5455,\n          \"menuItemId\": 3924,\n          \"courseId\": 2424,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2424,\n            \"dispNameNl\": \"Panini 'kalkoen-pesto'\",\n            \"dispNameEn\": \"Turkey pesto panini\",\n            \"nameNl\": \"panini 'kalkoen-pesto', z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": null,\n            \"preparation\": \"vanaf nu pestospread gran'olivia gebruiken - sandwich spread opwerken\",\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2424,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 2424,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3919,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5449,\n          \"menuItemId\": 3919,\n          \"courseId\": 3161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3161,\n            \"dispNameNl\": \"Bagel pumpkin\",\n            \"dispNameEn\": \"Pumpkin bagel\",\n            \"nameNl\": \"00 bagel pumpkin (cottage cheese,geroosterde pompoen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"pompoen met olijfolie en pezo roosteren in de oven. bestrijk 1 kant van de bagel met de cottage cheese, beleg met de geroosterde pompoen en werk af met gecrunchte walnoten.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3914,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5444,\n          \"menuItemId\": 3914,\n          \"courseId\": 4975,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4975,\n            \"dispNameNl\": \"Salade Marrakech veganlicious \",\n            \"dispNameEn\": \"Marrakesh veganlicious salad \",\n            \"nameNl\": \"00 salade marrakech veganlicious(couscous,mozzarisella),dd,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 gr\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4410,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 634,\n      \"sortorder\": 10,\n      \"menuItemContents\": []\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_cmi.raw.json",
    "content": "{\n  \"id\": 542,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3326,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4617,\n          \"menuItemId\": 3326,\n          \"courseId\": 2530,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2530,\n            \"dispNameNl\": \"Bio-wortelsoep\",\n            \"dispNameEn\": \"Organic carrot soup\",\n            \"nameNl\": \"bio-wortelsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2530,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3354,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4643,\n          \"menuItemId\": 3354,\n          \"courseId\": 4267,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4267,\n            \"dispNameNl\": \"Bellaroma burger\",\n            \"dispNameEn\": \"Bella Roma burger\",\n            \"nameNl\": \"bellaroma burger (grill), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"vege burger broodje grillen langs beide kanten - vege burger op voorhand in oven garen - vege burger \\u00e0 la minute grillen - hamburger op broodje - geraspte mozzarella erover en afwerken met de gegrilde groenten en toefje postelein. serveren met: frietjes - steakhouse frieten - kroketten - aardappelwafeltjes - gebakken krieltjes - parelcouscous met borlotti bonen - wedges - aardappel in de schil\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3332,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4623,\n          \"menuItemId\": 3332,\n          \"courseId\": 985,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 985,\n            \"dispNameNl\": \"kroketten\",\n            \"dispNameEn\": \"croquettes\",\n            \"nameNl\": \"kroketten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 985,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 985,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4621,\n          \"menuItemId\": 3332,\n          \"courseId\": 1033,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1033,\n            \"dispNameNl\": \"erwten en wortelen\",\n            \"dispNameEn\": \"peas and carrots\",\n            \"nameNl\": \"erwten en wortelen\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1033,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 4619,\n          \"menuItemId\": 3332,\n          \"courseId\": 1364,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1364,\n            \"dispNameNl\": \"Kalkoenlapje\",\n            \"dispNameEn\": \"Turkey escalope\",\n            \"nameNl\": \"kalkoenlapje dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": \"bak de filets in een braadslede, kruiden met kippenkruiden, verder garen in de oven, versnijden.\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1364,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4110,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5712,\n          \"menuItemId\": 4110,\n          \"courseId\": 5561,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5561,\n            \"dispNameNl\": \"Buddha bowl met zalm\",\n            \"dispNameEn\": \"buddha bowl with salmon\",\n            \"nameNl\": \"Buddha bowl met zalm,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - Maak de wasabidressing: meng de yoghurtdressing met de wasabi - Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: rijtje edamame bonen, sojascheuten, rijtje zalm en rijtje ma\\u00efs. Werk de budha bowl af met de zeewiersalade, koriander en zwart sesamzaad en tuinerwtenspread  met sjalot. Serveer hierbij de wasabimayonaise. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4101,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5704,\n          \"menuItemId\": 4101,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4106,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 5708,\n          \"menuItemId\": 4106,\n          \"courseId\": 3423,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3423,\n            \"dispNameNl\": \"Pasta met vegetarische bolognaise\",\n            \"dispNameEn\": \"Pasta with vegetarian bolognese\",\n            \"nameNl\": \"pasta vegetarische bolognaise, hzs (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"pasta met losstaand deksel opwarmen in de microgolfoven - gedurende ongeveer 4 minuten op 800 watt. - omroeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3423,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3423,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4086,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 5678,\n          \"menuItemId\": 4086,\n          \"courseId\": 4975,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4975,\n            \"dispNameNl\": \"Salade Marrakech veganlicious \",\n            \"dispNameEn\": \"Marrakesh veganlicious salad \",\n            \"nameNl\": \"00 salade marrakech veganlicious(couscous,mozzarisella),dd,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 gr\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4975,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4975,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4091,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 5683,\n          \"menuItemId\": 4091,\n          \"courseId\": 3161,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3161,\n            \"dispNameNl\": \"Bagel pumpkin\",\n            \"dispNameEn\": \"Pumpkin bagel\",\n            \"nameNl\": \"00 bagel pumpkin (cottage cheese,geroosterde pompoen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"pompoen met olijfolie en pezo roosteren in de oven. bestrijk 1 kant van de bagel met de cottage cheese, beleg met de geroosterde pompoen en werk af met gecrunchte walnoten.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3161,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3161,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4402,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 542,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 6082,\n          \"menuItemId\": 4402,\n          \"courseId\": 939,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 939,\n            \"dispNameNl\": \"tartaarsaus\",\n            \"dispNameEn\": \"tartar sauce\",\n            \"nameNl\": \"tartaarsaus koud\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"keuze in het gebruik van kappertjes 100ml of kappers 3l - mayonaise aanlengen met beetje water tot gladde saus . - peterselie en kappertjes toevoegen.- eieren grof cutteren en ook toevoegen - alles nog eens goed mengen. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 939,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 939,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 939,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 6083,\n          \"menuItemId\": 4402,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 6081,\n          \"menuItemId\": 4402,\n          \"courseId\": 1325,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1325,\n            \"dispNameNl\": \"Calamares\",\n            \"dispNameEn\": \"Calamari\",\n            \"nameNl\": \"calamares\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in friteuze op 180 \\u00b0 c\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1325,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1325,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 1325,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1325,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 6085,\n          \"menuItemId\": 4402,\n          \"courseId\": 5633,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5633,\n            \"dispNameNl\": \"saladbar\",\n            \"dispNameEn\": \"salad bar\",\n            \"nameNl\": \"saladbar met meerprijs\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5633,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_cmu.raw.json",
    "content": "{\n  \"id\": 513,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3178,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4406,\n          \"menuItemId\": 3178,\n          \"courseId\": 5011,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5011,\n            \"dispNameNl\": \"Bio-pompoensoep\",\n            \"dispNameEn\": \"Organic pumpkin soup\",\n            \"nameNl\": \"00 bio-pompoensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml/700 ml\",\n            \"extra\": null,\n            \"preparation\": \"groenten (ajuin,prei, pompoen en aardappelen) samen met bouillon beetgaar koken. - mixen en room toevoegen. op smaak brengen met peper en zout.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5011,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 5011,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3181,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4409,\n          \"menuItemId\": 3181,\n          \"courseId\": 5039,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5039,\n            \"dispNameNl\": \"Vegetarische lasagne\",\n            \"dispNameEn\": \"Vegetarian lasagne\",\n            \"nameNl\": \"lasagne vegetarisch kant-en klaar\",\n            \"nameEn\": \"\",\n            \"weight\": \"400 g\",\n            \"extra\": null,\n            \"preparation\": \"de lasagne in porties snijden. - 20 minuten opwaren in een oven van 200\\u00b0c. - eventueel van verpakking ontdoen en in een gastronorm bak opwarmen. - geraspte kaas erbij serveren.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5039,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5039,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5039,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5039,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5039,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3179,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4407,\n          \"menuItemId\": 3179,\n          \"courseId\": 5552,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5552,\n            \"dispNameNl\": \"Cuban Basmati Bowl\",\n            \"dispNameEn\": \"cuban basmati bowl\",\n            \"nameNl\": \"Cuban Basmati Bowl, w (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen.  Maak de zoete aardappel klaar volgens de fiche en breng op smaak met kaneel. Maak de bowl: doe de rijst in een kommetje, vervolgens de bonen, zoete aardappel, ma\\u00efs & rode kool.  Doe in het midden de guacamole, werk af met de cashewnoten en peterselie. Serveer hierbij de vinaigrette.Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5552,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5552,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5552,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5552,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5552,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3182,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 4410,\n          \"menuItemId\": 3182,\n          \"courseId\": 5027,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5027,\n            \"dispNameNl\": \"Croque brie\",\n            \"dispNameEn\": \"Brie grilled cheese sandwich\",\n            \"nameNl\": \"00 croque brie, z&w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"veggie\",\n            \"preparation\": \"1 potje saus is inbegrepen in de prijs\",\n            \"price\": 2.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5027,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5027,\n                \"allergenId\": 205\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5027,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5027,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": true,\n            \"enabled\": false,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3180,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 4408,\n          \"menuItemId\": 3180,\n          \"courseId\": 3874,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3874,\n            \"dispNameNl\": \"Peperkoeken geitenkaasje\",\n            \"dispNameEn\": \"Spiced bread and goat cheese\",\n            \"nameNl\": \"peperkoeken geitenkaasje, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"citroensap voor de bio-appel - peperkoek in reepjes snijden en bovenaan in de saladebox\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3874,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3874,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3183,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 4411,\n          \"menuItemId\": 3183,\n          \"courseId\": 25,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 25,\n            \"dispNameNl\": \"Wit broodje met kaas en wintergroenen\",\n            \"dispNameEn\": \"White roll with cheese and winter vegetables\",\n            \"nameNl\": \"wit broodje met kaas en wintergroentjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"135g / 275g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 25,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 25,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 25,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 25,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 25,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 25,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 25,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3184,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 4412,\n          \"menuItemId\": 3184,\n          \"courseId\": 4627,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4627,\n            \"dispNameNl\": \"Focaccia selder-rosbief\",\n            \"dispNameEn\": \"Celery and roast beef focaccia\",\n            \"nameNl\": \"00 focaccia selder-rosbief, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 4627,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4627,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 4627,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3185,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 513,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 4413,\n          \"menuItemId\": 3185,\n          \"courseId\": 5018,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5018,\n            \"dispNameNl\": \"Club pompernikkel-zoete aardappel\",\n            \"dispNameEn\": \"Pumpernickel and sweet potato club sandwich\",\n            \"nameNl\": \"00 club pompernikkel - zoete aardappel,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"conceptbroodje winter werk in 3 lagen met het roggebrood.- besmeer de 3 sneden brood met de wortelspread.- leg op de eerste boterham de schijfjes zoete aardappel, veldsla en farmer sla- leg de 2de boterham hierop en herhaal met het beleg en sluit met het laatste sneetje brood.- snijd de boterham diagonaal doormidden en doe er een cocktailprikker in\",\n            \"price\": 2.7,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5018,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5018,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5018,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_cst.raw.json",
    "content": "{\n  \"id\": 629,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3848,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5357,\n          \"menuItemId\": 3848,\n          \"courseId\": 2522,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2522,\n            \"dispNameNl\": \"Bio-jardini\\u00e8re soep\",\n            \"dispNameEn\": \"Organic jardini\\u00e8re soup\",\n            \"nameNl\": \"bio-jardini\\u00e8re soep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"garnituur: paprikablokjes\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2522,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2522,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2522,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3849,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5359,\n          \"menuItemId\": 3849,\n          \"courseId\": 981,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 981,\n            \"dispNameNl\": \"gebakken krielaardappelen\",\n            \"dispNameEn\": \"fried new potatoes\",\n            \"nameNl\": \"gebakken (kriel) aardappelen\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"stoom de aard.3/4 gaar en laat afkoelen - afbakken in margarine en olijfolie\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5358,\n          \"menuItemId\": 3849,\n          \"courseId\": 4267,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4267,\n            \"dispNameNl\": \"Bellaroma burger\",\n            \"dispNameEn\": \"Bella Roma burger\",\n            \"nameNl\": \"bellaroma burger (grill), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"vege burger broodje grillen langs beide kanten - vege burger op voorhand in oven garen - vege burger \\u00e0 la minute grillen - hamburger op broodje - geraspte mozzarella erover en afwerken met de gegrilde groenten en toefje postelein. serveren met: frietjes - steakhouse frieten - kroketten - aardappelwafeltjes - gebakken krieltjes - parelcouscous met borlotti bonen - wedges - aardappel in de schil\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4267,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4267,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3850,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5362,\n          \"menuItemId\": 3850,\n          \"courseId\": 981,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 981,\n            \"dispNameNl\": \"gebakken krielaardappelen\",\n            \"dispNameEn\": \"fried new potatoes\",\n            \"nameNl\": \"gebakken (kriel) aardappelen\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"stoom de aard.3/4 gaar en laat afkoelen - afbakken in margarine en olijfolie\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5361,\n          \"menuItemId\": 3850,\n          \"courseId\": 3206,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3206,\n            \"dispNameNl\": \"romanesco mix\",\n            \"dispNameEn\": \"romanesco mix\",\n            \"nameNl\": \"romanesco mix, z & w (+ extra 0,40 euro) \",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5360,\n          \"menuItemId\": 3850,\n          \"courseId\": 5048,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5048,\n            \"dispNameNl\": \"Kalkoengehaktballetjes\",\n            \"dispNameEn\": \"Turkey mince balls\",\n            \"nameNl\": \"00 kalkoengehaktballetjes\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"5 balletjes pp (20 g per stuk)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5048,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5048,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5048,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5048,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3880,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5397,\n          \"menuItemId\": 3880,\n          \"courseId\": 930,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 930,\n            \"dispNameNl\": \"portosaus\",\n            \"dispNameEn\": \"port sauce\",\n            \"nameNl\": \"portosaus dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"water aan de kook brengen - poeder oplossen in beetje water en toevoegen tot juiste dikte. - even laten inkoken 3 kg ajuin eminceren en aanstoven in margarine - bij saus voegen. - afwerken met peterselie\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 930,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 930,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5398,\n          \"menuItemId\": 3880,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5396,\n          \"menuItemId\": 3880,\n          \"courseId\": 3264,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3264,\n            \"dispNameNl\": \"Kipfilet op de grill \",\n            \"dispNameEn\": \"Grilled chicken breast \",\n            \"nameNl\": \"kipfilet op de grill 1 (kippenkruiden), dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3264,\n                \"courseLogoId\": 203\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3879,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5395,\n          \"menuItemId\": 3879,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5394,\n          \"menuItemId\": 3879,\n          \"courseId\": 1416,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1416,\n            \"dispNameNl\": \"African sunshinesaus\",\n            \"dispNameEn\": \"African sunshine sauce\",\n            \"nameNl\": \"african sunshine saus, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"courgettes en paprika aanstoven en kruiden. - mengen met de pasta en de african sunshine saus toevoegen.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1416,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1416,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3878,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5609,\n          \"menuItemId\": 3878,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3868,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 5382,\n          \"menuItemId\": 3868,\n          \"courseId\": 5563,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5563,\n            \"dispNameNl\": \"Falafelbowl\",\n            \"dispNameEn\": \"falafel bowl\",\n            \"nameNl\": \"Falafelbowl, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5563,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5563,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5563,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3906,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 5436,\n          \"menuItemId\": 3906,\n          \"courseId\": 5561,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5561,\n            \"dispNameNl\": \"Buddha bowl met zalm\",\n            \"dispNameEn\": \"buddha bowl with salmon\",\n            \"nameNl\": \"Buddha bowl met zalm,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - Maak de wasabidressing: meng de yoghurtdressing met de wasabi - Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: rijtje edamame bonen, sojascheuten, rijtje zalm en rijtje ma\\u00efs. Werk de budha bowl af met de zeewiersalade, koriander en zwart sesamzaad en tuinerwtenspread  met sjalot. Serveer hierbij de wasabimayonaise. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5561,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5561,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3861,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 629,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 5375,\n          \"menuItemId\": 3861,\n          \"courseId\": 3487,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3487,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - pastinaak grillen en mengen met de wortelen en sojascheuten en kokosschilfers - opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-02-13_hzs.raw.json",
    "content": "{\n  \"id\": 503,\n  \"menuDate\": \"2020-02-13T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3039,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 4230,\n          \"menuItemId\": 3039,\n          \"courseId\": 2526,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2526,\n            \"dispNameNl\": \"Bio-erwtensoep\",\n            \"dispNameEn\": \"Organic pea soup\",\n            \"nameNl\": \"bio-erwtensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3044,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 4235,\n          \"menuItemId\": 3044,\n          \"courseId\": 4629,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4629,\n            \"dispNameNl\": \"Bagel tzatziki\",\n            \"dispNameEn\": \"Tzatziki bagel\",\n            \"nameNl\": \"00 bagel tzatziki, z&w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"veggie\",\n            \"preparation\": null,\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4629,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4629,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4629,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3049,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 4240,\n          \"menuItemId\": 3049,\n          \"courseId\": 4168,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4168,\n            \"dispNameNl\": \"Broodje met cottage cheese en serranoham\",\n            \"dispNameEn\": \"Sandwich with cottage cheese and Serrano ham\",\n            \"nameNl\": \"broodje cottage cheese-serrano, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4168,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4168,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3054,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 4245,\n          \"menuItemId\": 3054,\n          \"courseId\": 4976,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4976,\n            \"dispNameNl\": \"Mexicaanse salade\",\n            \"dispNameEn\": \"Mexican salad\",\n            \"nameNl\": \"00 mexicaanse salade,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptsalade winter ma\\u00efs stomen.- de avocado in kleine blokjes/schijfjes snijden.- limoenen persen (een deel over de avocado doen voor verkleuring tegen te gaan)- quinoa koken (zie fiche).- olijfolie, rode bonen, wortelstaafjes, ma\\u00efs en avocado onder de quinoa mengen.- op smaak brengen met limoensap, komijn, look, peper en zout.- afwerken met vers gehakte koriander.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4976,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4976,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4976,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3059,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 4250,\n          \"menuItemId\": 3059,\n          \"courseId\": 2071,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2071,\n            \"dispNameNl\": \"Salade met perziken en zalmsalade\",\n            \"dispNameEn\": \"Salad with peaches and salmon salad\",\n            \"nameNl\": \"salade met perziken en zalmsalade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"zalm stomen en laten afkoelen (slechts max. 100g per persoon gebruiken) - zalm prakken, mengen met een kleine hoeveelheid mayonaise (voeg enkel extra mayo toe als de zalmsalade nog te droog is, laat de zalm in geen geval \\u2018zwemmen\\u2019 in de mayonaise), met geplette hardgekookte eieren en peterselie - perziken in partjes snijden - veldsla en rammenas mengen. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken volgorde voor vullen van onder naar boven: gesneden perziken (2 -3 stuks) + zalmsalade (+/- 80-100 gr) + gemengde veldsla \\u2013 rammenas\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2071,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 2071,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3064,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 4255,\n          \"menuItemId\": 3064,\n          \"courseId\": 5309,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5309,\n            \"dispNameNl\": \"Groentepizza\",\n            \"dispNameEn\": \"Vegetable pizza\",\n            \"nameNl\": \"00 groentepizza,dd,w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"pizzabodems openleggen en op een ingevette gastronorm plaatsen - tomatensaus over pizzabodems verdelen - courgette schijfjes grillen en op de pizza's schikken - pizza bakken in de oven - achteraf grana padano schilfers erover strooien en de curly kale en afkruiden met toegestane allergeenvrije kruiden. pizzabodem in 8 gelijke stukken snijden.\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5309,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5309,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5309,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5309,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3069,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 503,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 4260,\n          \"menuItemId\": 3069,\n          \"courseId\": 1093,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1093,\n            \"dispNameNl\": \"Pizzabaguette ham-kaas\",\n            \"dispNameEn\": \"Ham and cheese pizza baguette\",\n            \"nameNl\": \"pizza baguette ham/kaas, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1093,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1093,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1093,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_cde.raw.json",
    "content": "{\n  \"id\": 797,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5350,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 7297,\n          \"menuItemId\": 5350,\n          \"courseId\": 3622,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3622,\n            \"dispNameNl\": \"Bio-minestrone\",\n            \"dispNameEn\": \"Organic minestrone\",\n            \"nameNl\": \"bio-minestrone\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3622,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3622,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3622,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 3622,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 3622,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"groot: 1.20\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5313,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 7234,\n          \"menuItemId\": 5313,\n          \"courseId\": 975,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 975,\n            \"dispNameNl\": \"rijst met munt\",\n            \"dispNameEn\": \"rice with mint\",\n            \"nameNl\": \"rijst met munt\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de rijst gaar in licht gezouten water. - witte basmati ongeveer 10 min - volkoren basmati ongeveer 20 min - voeg er de verse munt aan toe,\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7232,\n          \"menuItemId\": 5313,\n          \"courseId\": 1304,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1304,\n            \"dispNameNl\": \"Quornburger 'southern style' \",\n            \"dispNameEn\": \"'Southern style\\u2019 quorn burger \",\n            \"nameNl\": \"quornburger southern style dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"2x63g\",\n            \"extra\": null,\n            \"preparation\": \"op platte plaatjes met bakpapier afbakken tot een temperatuur van minstens 65\\u00b0c bereikt is\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1304,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1304,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1304,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1304,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7233,\n          \"menuItemId\": 5313,\n          \"courseId\": 2008,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2008,\n            \"dispNameNl\": \"wokgroenten\",\n            \"dispNameEn\": \"stir-fried vegetables\",\n            \"nameNl\": \"wokgroenten, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2008,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2008,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2008,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2008,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5314,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 7236,\n          \"menuItemId\": 5314,\n          \"courseId\": 1052,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1052,\n            \"dispNameNl\": \"ratatouille\",\n            \"dispNameEn\": \"ratatouille\",\n            \"nameNl\": \"ratatouille\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1052,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7235,\n          \"menuItemId\": 5314,\n          \"courseId\": 1350,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1350,\n            \"dispNameNl\": \"Cordon bleu \",\n            \"dispNameEn\": \"Cordon bleu \",\n            \"nameNl\": \"cordon bleu varken\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": null,\n            \"preparation\": \"voorbakken in pan, kruiden en nadien afbakken in oven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1350,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1350,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7237,\n          \"menuItemId\": 5314,\n          \"courseId\": 3991,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3991,\n            \"dispNameNl\": \"parelcouscous\",\n            \"dispNameEn\": \"pearl couscous\",\n            \"nameNl\": \"parelcouscous\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3991,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5320,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 7249,\n          \"menuItemId\": 5320,\n          \"courseId\": 1432,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1432,\n            \"dispNameNl\": \"mediterraanse groentesaus\",\n            \"dispNameEn\": \"Mediterranean vegetable sauce\",\n            \"nameNl\": \"mediterraanse groentesaus, zvv, dd z&w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"pastasaus groenten aanstoven en bevochtigen met water. - kruiden toevoegen en een half uurtje laten pruttelen. - afbinden opgelet: vegan pasta voorzien\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1432,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1432,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1432,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7256,\n          \"menuItemId\": 5320,\n          \"courseId\": 5478,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5478,\n            \"dispNameNl\": \"Volkoren fusili\",\n            \"dispNameEn\": \"wholegrain fusili\",\n            \"nameNl\": \"Volkoren fusili, vegan\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": \"\",\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, olie toevoegen - de kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5478,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5325,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 7263,\n          \"menuItemId\": 5325,\n          \"courseId\": 1414,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1414,\n            \"dispNameNl\": \"Spaghetti carbonara\",\n            \"dispNameEn\": \"carbonara sauce\",\n            \"nameNl\": \"Spaghetti carbonara\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"witte saus poeder toevoegen aan kokend water en goed laten doorkoken. - culinaire room toevoegen en de kruiden. - terug laten koken en gebakken spekreepjes toevoegen. + gemalen kaas erbij serveren\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1414,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1414,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1414,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5330,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 7273,\n          \"menuItemId\": 5330,\n          \"courseId\": 922,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 922,\n            \"dispNameNl\": \"looksaus \",\n            \"dispNameEn\": \"garlic sauce \",\n            \"nameNl\": \"looksaus koud\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"mayonaise aanlengen met water tot gladde saus. - peterselie + lookpuree toevoegen en wederom goed mengen\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 922,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 922,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 922,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7278,\n          \"menuItemId\": 5330,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7268,\n          \"menuItemId\": 5330,\n          \"courseId\": 1078,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1078,\n            \"dispNameNl\": \"Scampibrochette\",\n            \"dispNameEn\": \"Scampi skewer\",\n            \"nameNl\": \"scampi brochette, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"100g pp\",\n            \"extra\": null,\n            \"preparation\": \"insmeren met mengeling van saus lemon/green peper en soja olie - grillen\",\n            \"price\": 5.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1078,\n                \"allergenId\": 207\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1078,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 1078,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5336,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 7284,\n          \"menuItemId\": 5336,\n          \"courseId\": 5566,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5566,\n            \"dispNameNl\": \"Teriyaki Chicken Bowl\",\n            \"dispNameEn\": \"teriyaki chicken bowl\",\n            \"nameNl\": \"Teriyaki Chicken Bowl, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . Breng de kip op smaak met de look en de wokpasta - Maakt de tuinerwtenspread volgens de receptuur. Maak de budhabowl als volgt: verdeel de rijst over het kommetje, leg in regenboogvorm: thaise tuinerwtenspread, rijtje edamame bonen, reepjes boerenkool, geraspte knolselder (besprenkel met citroensap), waterkers en kip. Werk af met het zwart sesamzaad.Serveer met de sojasaus vinaigrette.Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5566,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5566,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5566,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5566,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5566,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5566,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5566,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5342,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 797,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 7290,\n          \"menuItemId\": 5342,\n          \"courseId\": 5024,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5024,\n            \"dispNameNl\": \"Pittige pastinaak met humus en bulgur\",\n            \"dispNameEn\": \"Spicy parsnip with hummus and bulghur\",\n            \"nameNl\": \"00 pittige pastinaak met humus en rode quinoa/bulgur w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"veggie\",\n            \"preparation\": \"de pastinaak bakken in de oven met komijn, paprikapoeder, honing en olie.- tot deze gaar zijn & gekarameliseerd. meng de bulgur met de pastinaak en pompoenpitten.- hak de koriander fijn en verdeel over de salade samen met de veldsla\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5024,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5024,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5024,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5024,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5024,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5024,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5024,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_cgb.raw.json",
    "content": "{\n  \"id\": 807,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5418,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 7404,\n          \"menuItemId\": 5418,\n          \"courseId\": 2523,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2523,\n            \"dispNameNl\": \"Bio-bloemkoolsoep\",\n            \"dispNameEn\": \"Organic cauliflower soup\",\n            \"nameNl\": \"bio-bloemkoolsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"water aan de kook brengen. - 20 kg bloemkool, aardappel en ajuin toevoegen. - bouillon peper zout toevoegen en laten koken. - als groenten gaar.+ 5 kg bloemkoolroosjes op laatste moment toevoegen.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5428,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 7413,\n          \"menuItemId\": 5428,\n          \"courseId\": 5650,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5650,\n            \"dispNameNl\": \"Pasta met schorseneren in lookboter, met ei, olijven, peultjes & spekjes\",\n            \"dispNameEn\": \"Pasta with salsify in garlic butter, with egg, olives, snow peas & cubed bacons\",\n            \"nameNl\": \"Pasta met schorseneren in lookboter, met ei, olijven, peultjes & spekjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"beetgare pasta voorzichtig mengen met voorgesteamde peultjes en schorseneren, grof gesneden eitjes, look, olijven, peterselie en gesmolten boter -  gebakken spekblokjes toevoegen - afkruiden met pezo. - in gn van 6 diep verdelen en opwarmen in combi-steamer. \",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5650,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5650,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5431,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 7416,\n          \"menuItemId\": 5431,\n          \"courseId\": 5573,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5573,\n            \"dispNameNl\": \"Purple bowl\",\n            \"dispNameEn\": \"purple bowl\",\n            \"nameNl\": \"Purple bowl,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de parelcouscous klaar volgens de bereidingswijze op de verpakking & laat afkoelen . - Maak de budhabowl als volgt: vul het kommetje met de parelcouscous, vervolgens de bonen, rode bietblokjes, geraspte knolselder en een paar stukjes witloof aan de zijkant. Werk de budha bowl af met kippenreepjes, gecrunshte pindanoten, gebakken uitjes & peterselie. Serveer hierbij de vinaigrette sjalot & ui. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5573,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5573,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5413,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 7399,\n          \"menuItemId\": 5413,\n          \"courseId\": 3104,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3104,\n            \"dispNameNl\": \"Broodje sjeik\",\n            \"dispNameEn\": \"Sjeik sandwich\",\n            \"nameNl\": \"broodje sjeik,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptbroodje winter eerst tomatade - dan augurken - mozzarisella - postelein\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3104,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3104,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3104,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3104,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5407,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 7394,\n          \"menuItemId\": 5407,\n          \"courseId\": 1890,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1890,\n            \"dispNameNl\": \"Salade zonder sla\",\n            \"dispNameEn\": \"Salad without lettuce\",\n            \"nameNl\": \"salade slaatje zonder sla (aardappel, appel, raapjes), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton appel besprenkelen met citroensap\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5424,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 807,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 7409,\n          \"menuItemId\": 5424,\n          \"courseId\": 1084,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1084,\n            \"dispNameNl\": \"Croque Hawa\\u00ef\",\n            \"dispNameEn\": \"Croque Hawaii\",\n            \"nameNl\": \"croque hawa\\u00ef, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"keuze uit wit brood of bruin brood om de croque te maken 1 potje saus is inbegrepen in de prijs\",\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1084,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1084,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1084,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1084,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1084,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1084,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_cmi.parsed.expected.yaml",
    "content": "$test_case:\n  course_of_interest: 5398\n  reason: |\n    On this response, Komidabot differred from the official site by only showing one price for the \"Purple bowl\" menu\n    item. The reason for this is we only used to look if \"calculatedMultiplePrices\" is set to true, but\n    \"fixedMultiplePrices\" set to true should behave the same way (at least to display menu items).\ncampus: cmi\ndate: '2020-03-12'\nmenu:\n- components:\n  - allergens: []\n    attributes:\n    - BIO\n    - SOUP\n    - VEGAN\n    name:\n      en: Organic cauliflower soup\n      nl: Bio-bloemkoolsoep\n  external_id: 3657\n  multiple_prices: false\n  price: '0.90'\n  sort_order: 0\n- components:\n  - allergens:\n    - EGG\n    - LUPINE\n    - MILK_LACTOSE\n    - NUTS\n    - SESAME\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - VEGGIE\n    name:\n      en: Vegetarian vol-au-vent\n      nl: Veggie koninginnehapje\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: fries\n      nl: frieten\n  - allergens:\n    - EGG\n    - MILK_LACTOSE\n    - NUTS\n    attributes: []\n    name:\n      en: Belgian endive salad with nuts, w\n      nl: witloofsla met noten, w\n  external_id: 3658\n  multiple_prices: true\n  price: '5.40'\n  sort_order: 1\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - LUPINE\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - SESAME\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    name:\n      en: Chicken vol-au-vent\n      nl: Koninginnehapje\n  - allergens:\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: fries\n      nl: frieten\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - LUPINE\n    - MILK_LACTOSE\n    - MOLLUSKS\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes: []\n    name:\n      en: \"crudit\\xE9s\"\n      nl: Rauwkostslaatje\n  external_id: 3659\n  multiple_prices: true\n  price: '5.40'\n  sort_order: 2\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - FISH\n    - MILK_LACTOSE\n    - NUTS\n    - PEANUTS\n    - SESAME\n    attributes:\n    - SALAD\n    - VEGGIE\n    name:\n      en: Salad without lettuce\n      nl: Salade zonder sla\n  external_id: 5391\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 6\n- components:\n  - allergens:\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - SNACK\n    - VEGAN\n    name:\n      en: Sjeik sandwich\n      nl: Broodje sjeik\n  external_id: 5396\n  multiple_prices: false\n  price: '3.60'\n  sort_order: 7\n- components:\n  - allergens:\n    - CELERY\n    - NUTS\n    - PEANUTS\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    - SALAD\n    name:\n      en: purple bowl\n      nl: Purple bowl\n  external_id: 5398\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 5\n- components:\n  - allergens:\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - VEGAN\n    name:\n      en: paprika cream with garden cress\n      nl: pasta met paprikaroom & waterkers\n  external_id: 5505\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 3\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - PIG\n    name:\n      en: Pasta with salsify in garlic butter, with egg, olives, snow peas & cubed\n        bacons\n      nl: Pasta met schorseneren in lookboter, met ei, olijven, peultjes & spekjes\n  external_id: 5510\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens:\n    - EGG\n    - MUSTARD\n    - WHEAT_GLUTEN\n    attributes:\n    - GRILL\n    - VEGGIE\n    name:\n      en: New Orleans pepper burger\n      nl: New Orleans pepper burger\n  external_id: 5539\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 5\n"
  },
  {
    "path": "tests/external_menus/2020-03-12_cmi.raw.json",
    "content": "{\n  \"id\": 599,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 3657,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5066,\n          \"menuItemId\": 3657,\n          \"courseId\": 2523,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2523,\n            \"dispNameNl\": \"Bio-bloemkoolsoep\",\n            \"dispNameEn\": \"Organic cauliflower soup\",\n            \"nameNl\": \"bio-bloemkoolsoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"water aan de kook brengen. - 20 kg bloemkool, aardappel en ajuin toevoegen. - bouillon peper zout toevoegen en laten koken. - als groenten gaar.+ 5 kg bloemkoolroosjes op laatste moment toevoegen.\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2523,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3658,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5068,\n          \"menuItemId\": 3658,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5067,\n          \"menuItemId\": 3658,\n          \"courseId\": 1312,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1312,\n            \"dispNameNl\": \"Veggie koninginnehapje\",\n            \"dispNameEn\": \"Vegetarian vol-au-vent\",\n            \"nameNl\": \"koninginnehapje veggie dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"450g pp\",\n            \"extra\": null,\n            \"preparation\": \"( zo kort mogelijk voor service bereiden ! ) quornsstuks en opwarmen, samen met de gebakken ajuin en champignons mengen in diepe bak. - warme witte saus + groentenbouillon erover gieten en mengen tot juiste consistentie. - volledig afsmaken en afwerken met peterselie - + pasteitjes 5 min. opwarmen in oven\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1312,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1312,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5069,\n          \"menuItemId\": 3658,\n          \"courseId\": 1623,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1623,\n            \"dispNameNl\": \"witloofsla met noten, w\",\n            \"dispNameEn\": \"Belgian endive salad with nuts, w\",\n            \"nameNl\": \"witloofsla met noten, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1623,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1623,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1623,\n                \"allergenId\": 205\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 3659,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5072,\n          \"menuItemId\": 3659,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5070,\n          \"menuItemId\": 3659,\n          \"courseId\": 1378,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1378,\n            \"dispNameNl\": \"Koninginnehapje\",\n            \"dispNameEn\": \"Chicken vol-au-vent\",\n            \"nameNl\": \"koninginnehapje dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"( zo kort mogelijk voor service bereiden ! ) kippenreepjes en balletjes opwarmen en samen met de gebakken ajuin en champignons mengen in diepe bak warme witte saus + bouillon van kip erover gieten en mengen tot juiste consistentie. - volledig afsmaken en afwerken met peterselie - + pasteitjes 5 min. opwarmen in oven\",\n            \"price\": 5.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1378,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 5073,\n          \"menuItemId\": 3659,\n          \"courseId\": 5514,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5514,\n            \"dispNameNl\": \"Rauwkostslaatje\",\n            \"dispNameEn\": \"crudit\\u00e9s\",\n            \"nameNl\": \"rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is een slaatje om bij een gerecht toe te voegen, waarbij de salade al in de prijs gerekend is.\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5505,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 7527,\n          \"menuItemId\": 5505,\n          \"courseId\": 1425,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1425,\n            \"dispNameNl\": \"pasta met paprikaroom & waterkers\",\n            \"dispNameEn\": \"paprika cream with garden cress\",\n            \"nameNl\": \"00 paprikaroom met waterkers, zvv, dd, z & w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux afsmaken met pezo en proven\\u00e7aalse kruiden. - rode paprika gaar steamen en samen met room bij tomatensaus doen - mixen en eventueel verdunnen/aandikken serveer met een vegan pasta ( volkoren spaghetti speciality anco/ fusili volkoren bio)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5510,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 7531,\n          \"menuItemId\": 5510,\n          \"courseId\": 5650,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5650,\n            \"dispNameNl\": \"Pasta met schorseneren in lookboter, met ei, olijven, peultjes & spekjes\",\n            \"dispNameEn\": \"Pasta with salsify in garlic butter, with egg, olives, snow peas & cubed bacons\",\n            \"nameNl\": \"Pasta met schorseneren in lookboter, met ei, olijven, peultjes & spekjes\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"beetgare pasta voorzichtig mengen met voorgesteamde peultjes en schorseneren, grof gesneden eitjes, look, olijven, peterselie en gesmolten boter -  gebakken spekblokjes toevoegen - afkruiden met pezo. - in gn van 6 diep verdelen en opwarmen in combi-steamer. \",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5650,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5650,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5650,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5398,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 7383,\n          \"menuItemId\": 5398,\n          \"courseId\": 5573,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5573,\n            \"dispNameNl\": \"Purple bowl\",\n            \"dispNameEn\": \"purple bowl\",\n            \"nameNl\": \"Purple bowl,w\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Maak de parelcouscous klaar volgens de bereidingswijze op de verpakking & laat afkoelen . - Maak de budhabowl als volgt: vul het kommetje met de parelcouscous, vervolgens de bonen, rode bietblokjes, geraspte knolselder en een paar stukjes witloof aan de zijkant. Werk de budha bowl af met kippenreepjes, gecrunshte pindanoten, gebakken uitjes & peterselie. Serveer hierbij de vinaigrette sjalot & ui. Zet het deksel op de weckpot en plak het etiket aan de zijkant.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5573,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5573,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5573,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5539,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 7561,\n          \"menuItemId\": 5539,\n          \"courseId\": 4269,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4269,\n            \"dispNameNl\": \"New Orleans pepper burger\",\n            \"dispNameEn\": \"New Orleans pepper burger\",\n            \"nameNl\": \"new orleans pepper burger (grill), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"saus maken met dressing maison - mayo - mosterd (keuze uit 1 liter of 3 liter) - bbq verstegen saus - groentenmengeling maken met witte kool, geraspte wortel, postelein, bieslook en peterselie. - hamburger broodje grillen langs beide kanten - burger op voorhand in oven garen - burger \\u00e0 la minute grillen - saus op broodje doen - burger erop en afwerken met de groentjes en gebakken ajuintjes - serveren met: frietjes - steakhouse frieten - kroketten - aardappelwafeltjes - gebakken krieltjes - parelcouscous met borlotti bonen - wedges - aardappel in de schil\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4269,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4269,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4269,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4269,\n                \"courseLogoId\": 203\n              },\n              {\n                \"courseId\": 4269,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5391,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 7376,\n          \"menuItemId\": 5391,\n          \"courseId\": 1890,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1890,\n            \"dispNameNl\": \"Salade zonder sla\",\n            \"dispNameEn\": \"Salad without lettuce\",\n            \"nameNl\": \"salade slaatje zonder sla (aardappel, appel, raapjes), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton appel besprenkelen met citroensap\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5396,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 599,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 7381,\n          \"menuItemId\": 5396,\n          \"courseId\": 3104,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3104,\n            \"dispNameNl\": \"Broodje sjeik\",\n            \"dispNameEn\": \"Sjeik sandwich\",\n            \"nameNl\": \"broodje sjeik,w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptbroodje winter eerst tomatade - dan augurken - mozzarisella - postelein\",\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3104,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3104,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3104,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 3104,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_cmu.raw.json",
    "content": "{\n  \"id\": 748,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 4793,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 6610,\n          \"menuItemId\": 4793,\n          \"courseId\": 5533,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5533,\n            \"dispNameNl\": \"Winter falafel bowl\",\n            \"dispNameEn\": \"Winter falafel bowl\",\n            \"nameNl\": \"Winter falafel bowl, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Mix de opgelegde rode biet, breng op smaak met het limoensap & pezo. Meng de rode bietspread onder de gekookte parelcouscous. Maak de buddha bowl als volgt: verdeel de rode parelcouscous over de kommetjes, vervolgens de gesneden rode kool, gele & oranje wortelschijfjes, curly kale en werk af met de falafelballetjes, pompoenpitten & een toefje hummus\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5533,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5533,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5533,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5533,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5533,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4801,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 6618,\n          \"menuItemId\": 4801,\n          \"courseId\": 5033,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5033,\n            \"dispNameNl\": \"Quesadilla's veganlicious\",\n            \"dispNameEn\": \"Veganlicious quesadillas\",\n            \"nameNl\": \"00 quesadilla's veganlicious, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"snack winter bak de diepgevroren quinoa-groenekoolburger op een bakplaat of in de oven.- strooi de mozzarisella over de bodem van de wraps en voeg de lente-ui toe.- voeg een laagje geroosterde ma\\u00efs toe. leg de burger in de wrap en vouw toe tot een quesadilla.- voeg de quesadilla\\\"s toe aan een warme braadpan en bak elke kant een paar minuten op een laag tot middelhoog vuur tot ze licht geroosterd zijn of je kan ze ook in een paninitoestel plaatsen.- snijd in hapklare stukken.\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5033,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5033,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5033,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5033,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4805,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 6641,\n          \"menuItemId\": 4805,\n          \"courseId\": 159,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 159,\n            \"dispNameNl\": \"Abdijbroodje \",\n            \"dispNameEn\": \"Abbey roll \",\n            \"nameNl\": \"abdijbroodje (baguelino broodje, smeerkaas, rammenas) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 159,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 159,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4811,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 6627,\n          \"menuItemId\": 4811,\n          \"courseId\": 4990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4990,\n            \"dispNameNl\": \"Kruidig roggebrood\",\n            \"dispNameEn\": \"Herbed rye bread\",\n            \"nameNl\": \"00 kruidig roggebrood, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"vegan\",\n            \"preparation\": \"conceptbroodje winter werk in 3 lagen met het roggebrood.- besmeer de 3 sneden brood met de vegane mayonaise.- leg op de eerste boterham de vegan kruidenkaas, de schijfjes raap en de waterkers.- leg de 2de boterham hierop en herhaal met het beleg en sluit met het laatste sneetje brood.- snijd de boterham diagonaal doormidden en doe er een cocktailprikker in\",\n            \"price\": 2.7,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4990,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4990,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4990,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4816,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 6632,\n          \"menuItemId\": 4816,\n          \"courseId\": 4953,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4953,\n            \"dispNameNl\": \"Meergranenbroodje met basilicum-humus en wintergroenten\",\n            \"dispNameEn\": \"Multigrain roll with basil hummus and winter vegetables\",\n            \"nameNl\": \"00 broodje fit met humus basilicum en wintergroenten (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"135 g/275g\",\n            \"extra\": null,\n            \"preparation\": \"broodje winter\",\n            \"price\": 3.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4953,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4953,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4953,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4953,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4953,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4821,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 6636,\n          \"menuItemId\": 4821,\n          \"courseId\": 1872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1872,\n            \"dispNameNl\": \"Italiaanse worteltartaar met parmezaan\",\n            \"dispNameEn\": \"Italian carrot tartare with parmesan\",\n            \"nameNl\": \"italiaanse worteltartaar met parmezaan, dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1872,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4826,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 6642,\n          \"menuItemId\": 4826,\n          \"courseId\": 159,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 159,\n            \"dispNameNl\": \"Abdijbroodje \",\n            \"dispNameEn\": \"Abbey roll \",\n            \"nameNl\": \"abdijbroodje (baguelino broodje, smeerkaas, rammenas) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 159,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 159,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 159,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4831,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 6647,\n          \"menuItemId\": 4831,\n          \"courseId\": 1924,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1924,\n            \"dispNameNl\": \"Salade Wicca\",\n            \"dispNameEn\": \"Wicca salad\",\n            \"nameNl\": \"salade wicca (zalm, linzen, aardappelen), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"pompoenblokjes en aardappelen: niet natuur stomen, maar garen in de oven met olijfolie en rozemarijn. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1924,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1924,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4832,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 6648,\n          \"menuItemId\": 4832,\n          \"courseId\": 864,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 864,\n            \"dispNameNl\": \"Paprikasoep\",\n            \"dispNameEn\": \"Bell pepper soup\",\n            \"nameNl\": \"paprikasoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 864,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 864,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 864,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 864,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4833,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 6649,\n          \"menuItemId\": 4833,\n          \"courseId\": 5205,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5205,\n            \"dispNameNl\": \"Quiche normande\",\n            \"dispNameEn\": \"Quiche normande\",\n            \"nameNl\": \"00 quiche normande z&w\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"d + 1 indien niet opgewarmd - warm op volgens instructies\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5205,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5205,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5205,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5205,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5546,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 748,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7567,\n          \"menuItemId\": 5546,\n          \"courseId\": 4404,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4404,\n            \"dispNameNl\": \"Chia-banaanpudding\",\n            \"dispNameEn\": \"Chia and banana pudding\",\n            \"nameNl\": \"chia banaanpudding (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"te maken in kleine cambiopot of in glazen dessertpotje of tumbler (desserten per 25 pers.)\",\n            \"preparation\": \"dessert mix de bananen met de kokosmelk (variant amandelmelk is minder vet), vanille, kaneel en ahornsiroop. - meng er daarna het chiazaad onder en verdeel: een laagje pudding, een paar schijfjes banaan, laagje pudding en afwerken met kokosschilfers\",\n            \"price\": 2.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4404,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4404,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4404,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4404,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_cst.raw.json",
    "content": "{\n  \"id\": 792,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5259,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 7129,\n          \"menuItemId\": 5259,\n          \"courseId\": 2526,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2526,\n            \"dispNameNl\": \"Bio-erwtensoep\",\n            \"dispNameEn\": \"Organic pea soup\",\n            \"nameNl\": \"bio-erwtensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": null,\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2526,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5679,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 7705,\n          \"menuItemId\": 5679,\n          \"courseId\": 1476,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1476,\n            \"dispNameNl\": \"Groentepa\\u00eblla\",\n            \"dispNameEn\": \"Vegetable paella\",\n            \"nameNl\": \"groentepaella, zvv, dd, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"rijst half gaar koken in water met curry en curcuma - erwtjes, paprika, boontjes, wortelen en spruitjes beetgaar voorsteamen. - champignons snijden en aanbakken - bio bouillon oplossen in witte wijn samen met paella mix, pezo, saffraan en lookpasta. - rijst, bouillonmix samen in pan . - mengen en even opbakken - in gastro's van 6cm scheppen, samen met de groentjes en in combi steam (150\\u00b0) opwarmen tot gewenste temp. - uitscheppen en afwerken met citroentje en peterselie.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1476,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1476,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1476,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5680,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 7706,\n          \"menuItemId\": 5680,\n          \"courseId\": 1455,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1455,\n            \"dispNameNl\": \"Pa\\u00eblla met kip en zeevruchten\",\n            \"dispNameEn\": \"Chicken and seafood paella\",\n            \"nameNl\": \"paella met kip en zeevruchten, dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"rijst half gaar koken in water met curcuma - erwtjes en paprika beetgaar voorsteamen. - kip en visbouillon oplossen in witte wijn samen met pezo en lookpasta. - rijst, kip, groenten, mosselvlees, surimiflakes, bouillonmix samen in pan . - mengen en even opbakken - in gastro's van 6cm scheppen en in combi steam (150\\u00b0)opwarmen tot gewenste temp. - uitscheppen en afwerken met 2 ringetjes gefrituurde calamares, citroentje en peterselie en de vooraf gebakken kippenonderboutjes,\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 1455,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1455,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1455,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5295,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 7214,\n          \"menuItemId\": 5295,\n          \"courseId\": 932,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 932,\n            \"dispNameNl\": \"Proven\\u00e7aalse saus\",\n            \"dispNameEn\": \"Proven\\u00e7al sauce\",\n            \"nameNl\": \"proven\\u00e7aalse saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux - afsmaken met pezo en proven\\u00e7aalse kruiden. ajuin en champignons aanstoven en kleuren, courgetten (schijven nog eventueel halveren) en paprika toevoegen warme tomatensaus toevoegen en afwerken met proven\\u00e7aalse kruiden en peterselie. - eventueel verdunnen met heet water\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 932,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 932,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7215,\n          \"menuItemId\": 5295,\n          \"courseId\": 985,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 985,\n            \"dispNameNl\": \"kroketten\",\n            \"dispNameEn\": \"croquettes\",\n            \"nameNl\": \"kroketten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 985,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 985,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7212,\n          \"menuItemId\": 5295,\n          \"courseId\": 1322,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1322,\n            \"dispNameNl\": \"Sojasteak\",\n            \"dispNameEn\": \"Soy steak\",\n            \"nameNl\": \"sojasteak dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"67g\",\n            \"extra\": null,\n            \"preparation\": \"friteuse voorverwarmen op 180\\u00b0c. - steaks kleuren en per 20 stuks op bakplaatjes schikken. - oven voorverwarmen op 180\\u00b0c burger verhitten tot kern van 65 graden bereikt is. - in bain-marie schikken. - 1 stuks pp 67 gram pp\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1322,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1322,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1322,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1322,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5291,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 7180,\n          \"menuItemId\": 5291,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7179,\n          \"menuItemId\": 5291,\n          \"courseId\": 3984,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3984,\n            \"dispNameNl\": \"gerookte zalm, dille en kruidenkaas\",\n            \"dispNameEn\": \"smoked salmon, dill and cream cheese\",\n            \"nameNl\": \"pasta met gerookte zalm, dille en kruidenkaas, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"ajuin snijden en aanstoven - primerba dille toevoegen en room en laten doorkoken - kruidenkaas doorroeren - afsmaken met pezono - broccoliroosjes steamen - kerstomaatjes halveren - groenten toevoegen aan de saus (kerstomaatjes pas op einde toevoegen) - gerookte zalmsnippers toevoegen.\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3984,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3984,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3984,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 3984,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3984,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3984,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5300,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 7197,\n          \"menuItemId\": 5300,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 7196,\n          \"menuItemId\": 5300,\n          \"courseId\": 1425,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1425,\n            \"dispNameNl\": \"pasta met paprikaroom & waterkers\",\n            \"dispNameEn\": \"paprika cream with garden cress\",\n            \"nameNl\": \"00 paprikaroom met waterkers, zvv, dd, z & w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"vegan\",\n            \"preparation\": \"tomatensaus maken: ajuin aanstoven - bestrooien met paprika, look, bloem en tomatenpuree - drogen - water toevoegen + tomatenblokjes - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux afsmaken met pezo en proven\\u00e7aalse kruiden. - rode paprika gaar steamen en samen met room bij tomatensaus doen - mixen en eventueel verdunnen/aandikken serveer met een vegan pasta ( volkoren spaghetti speciality anco/ fusili volkoren bio)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1425,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1425,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5263,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 7133,\n          \"menuItemId\": 5263,\n          \"courseId\": 1889,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1889,\n            \"dispNameNl\": \"Salade 'Forza'\",\n            \"dispNameEn\": \"Forza salad\",\n            \"nameNl\": \"salade 'forza' (bulgur, mozzarella), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1889,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1889,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1889,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1889,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5268,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 7138,\n          \"menuItemId\": 5268,\n          \"courseId\": 4190,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4190,\n            \"dispNameNl\": \"Salade Ankara met groentepat\\u00e9 \",\n            \"dispNameEn\": \"Ankara salad with vegetable pat\\u00e9 \",\n            \"nameNl\": \"salade ankara met groentepat\\u00e9 , w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"conceptsalade winter gril de pompoen. kook de quinoa, met de bouillion, kruid met de yedi baharat kruiden, meng met gesnipperde munt en rode ui en zwarte olijfschijven, leg hier op staafjes van de pat\\u00e9. meng de gare bonen en kikkererwten en de gegrilde pompoen. werk af met takjes platte peterselie.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4190,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4190,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4190,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4190,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4190,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4190,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 5271,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 792,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 7141,\n          \"menuItemId\": 5271,\n          \"courseId\": 4165,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4165,\n            \"dispNameNl\": \"Zuiderse pasta met serranoham\",\n            \"dispNameEn\": \"Mediterranean pasta with Serrano ham\",\n            \"nameNl\": \"salade zuiderse pasta met serrano, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"pasta mengen met cottage sheese, zongedroogde tomaat, gehakte platte peterselie, reepjes gegrilde paprika, olijfolie, peper. hier op de schijfjes olijf, serranoham, veldsla en zonnebloempitten serranoham in hapklare stukken snijden\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4165,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4165,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4165,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4165,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-12_hzs.raw.json",
    "content": "{\n  \"id\": 683,\n  \"menuDate\": \"2020-03-12T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 4179,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 5803,\n          \"menuItemId\": 4179,\n          \"courseId\": 3623,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3623,\n            \"dispNameNl\": \"Bio-tomatensoep\",\n            \"dispNameEn\": \"Organic tomato soup\",\n            \"nameNl\": \"bio-tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"bieslook op het einde toevoegen\",\n            \"price\": 0.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3623,\n                \"courseLogoId\": 201\n              },\n              {\n                \"courseId\": 3623,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 3623,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4184,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 5808,\n          \"menuItemId\": 4184,\n          \"courseId\": 403,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 403,\n            \"dispNameNl\": \"Sweet 'n cheesy \",\n            \"dispNameEn\": \"Sweet 'n cheesy \",\n            \"nameNl\": \"sweet 'n cheesy (smeerkaas, tapenade paprika) dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 403,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 403,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 403,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 403,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4189,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 5813,\n          \"menuItemId\": 4189,\n          \"courseId\": 1867,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1867,\n            \"dispNameNl\": \"Pikant chilibroodje\",\n            \"dispNameEn\": \"Spicy chilli sandwich\",\n            \"nameNl\": \"chilibroodje pikant (rosbief), w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kruiden met piri piri, mag pittig zijn\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1867,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1867,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1867,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1867,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1867,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4194,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 5818,\n          \"menuItemId\": 4194,\n          \"courseId\": 1890,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1890,\n            \"dispNameNl\": \"Salade zonder sla\",\n            \"dispNameEn\": \"Salad without lettuce\",\n            \"nameNl\": \"salade slaatje zonder sla (aardappel, appel, raapjes), dd, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton appel besprenkelen met citroensap\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1890,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4199,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 5823,\n          \"menuItemId\": 4199,\n          \"courseId\": 3871,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3871,\n            \"dispNameNl\": \"Mango-kip-salade\",\n            \"dispNameEn\": \"Mango and chicken salad\",\n            \"nameNl\": \"salade mango-kip, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3871,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3871,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3871,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4204,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 5828,\n          \"menuItemId\": 4204,\n          \"courseId\": 175,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 175,\n            \"dispNameNl\": \"Panini Marco Polo\",\n            \"dispNameEn\": \"Panini Marco Polo\",\n            \"nameNl\": \"panini marco polo (tapenade paprika, cheddar) dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"250 g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 2.9,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 175,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 175,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 175,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 175,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 175,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 175,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 175,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 4209,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 683,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 5833,\n          \"menuItemId\": 4209,\n          \"courseId\": 1111,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1111,\n            \"dispNameNl\": \"Fishburger\",\n            \"dispNameEn\": \"Fish burger\",\n            \"nameNl\": \"fishburger (met tartaarsaus), z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1111,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1111,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1111,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_cde.raw.json",
    "content": "{\n  \"id\": 799,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5834,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 799,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7910,\n          \"menuItemId\": 5834,\n          \"courseId\": 5684,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5684,\n            \"dispNameNl\": \"komida is minstens tot en met 3 april gesloten.\",\n            \"dispNameEn\": \"From 16/03/2020 till (at least) 3/04/2020 komida will be closed.\",\n            \"nameNl\": \"Vanaf 16/03/2020 tot en met 3/04/2020 is komida enkel een shop waar je terecht kan voor belegde broodjes, snoep en drank. Ter plaatse eten kan niet.\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_cgb.raw.json",
    "content": "{\n  \"id\": 825,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5839,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 825,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7915,\n          \"menuItemId\": 5839,\n          \"courseId\": 5684,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5684,\n            \"dispNameNl\": \"komida is minstens tot en met 3 april gesloten.\",\n            \"dispNameEn\": \"From 16/03/2020 till (at least) 3/04/2020 komida will be closed.\",\n            \"nameNl\": \"Vanaf 16/03/2020 tot en met 3/04/2020 is komida enkel een shop waar je terecht kan voor belegde broodjes, snoep en drank. Ter plaatse eten kan niet.\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_cmi.raw.json",
    "content": "{\n  \"id\": 602,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5846,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 602,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7922,\n          \"menuItemId\": 5846,\n          \"courseId\": 5684,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5684,\n            \"dispNameNl\": \"komida is minstens tot en met 3 april gesloten.\",\n            \"dispNameEn\": \"From 16/03/2020 till (at least) 3/04/2020 komida will be closed.\",\n            \"nameNl\": \"Vanaf 16/03/2020 tot en met 3/04/2020 is komida enkel een shop waar je terecht kan voor belegde broodjes, snoep en drank. Ter plaatse eten kan niet.\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_cmu.raw.json",
    "content": "{\n  \"id\": 750,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5885,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 750,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7960,\n          \"menuItemId\": 5885,\n          \"courseId\": 5685,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5685,\n            \"dispNameNl\": \"Gesloten van 16/03/2020 tot en met 17/04/2020\",\n            \"dispNameEn\": \"Closed from 16/03/2020 till 17/04/2020\",\n            \"nameNl\": \"Gesloten van 16/03/2020 tot en met 17/04/2020 \",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_cst.raw.json",
    "content": "{\n  \"id\": 814,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5851,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 814,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7927,\n          \"menuItemId\": 5851,\n          \"courseId\": 5684,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5684,\n            \"dispNameNl\": \"komida is minstens tot en met 3 april gesloten.\",\n            \"dispNameEn\": \"From 16/03/2020 till (at least) 3/04/2020 komida will be closed.\",\n            \"nameNl\": \"Vanaf 16/03/2020 tot en met 3/04/2020 is komida enkel een shop waar je terecht kan voor belegde broodjes, snoep en drank. Ter plaatse eten kan niet.\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-03-16_hzs.raw.json",
    "content": "{\n  \"id\": 685,\n  \"menuDate\": \"2020-03-16T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 5854,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 685,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 7930,\n          \"menuItemId\": 5854,\n          \"courseId\": 5685,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5685,\n            \"dispNameNl\": \"Gesloten van 16/03/2020 tot en met 17/04/2020\",\n            \"dispNameEn\": \"Closed from 16/03/2020 till 17/04/2020\",\n            \"nameNl\": \"Gesloten van 16/03/2020 tot en met 17/04/2020 \",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_cde.raw.json",
    "content": "{\n  \"id\": 954,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7329,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9507,\n          \"menuItemId\": 7329,\n          \"courseId\": 997,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 997,\n            \"dispNameNl\": \"puree\",\n            \"dispNameEn\": \"mashed potatoes\",\n            \"nameNl\": \"puree zelf bereid\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"aardappelen steamen. - in klopper tot puree verwerken met melk en margarine en afsmaken.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 997,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9492,\n          \"menuItemId\": 7329,\n          \"courseId\": 1328,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1328,\n            \"dispNameNl\": \"Kabeljauwfilet (MSC)\",\n            \"dispNameEn\": \"Cod fillet (MSC)\",\n            \"nameNl\": \"kabeljauw gestoomd (filet), msc, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"kabeljauw dag voordien openleggen en laten ontdooien. - dag zelf stomen op 90 \\u00b0c tot kerntemperatuur van 70 \\u00b0c - afkruiden\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1328,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1328,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9562,\n          \"menuItemId\": 7329,\n          \"courseId\": 3210,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3210,\n            \"dispNameNl\": \"fijne groentjes\",\n            \"dispNameEn\": \"garden vegetables\",\n            \"nameNl\": \"fijne groentjes, z & w (+ extra 0,40 euro)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7313,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9453,\n          \"menuItemId\": 7313,\n          \"courseId\": 5787,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5787,\n            \"dispNameNl\": \"Pasta met feta, olijven en paprika \",\n            \"dispNameEn\": \"Pasta with feta, olives and bell pepper \",\n            \"nameNl\": \"Pasta met feta, olijven en paprika \",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"\",\n            \"preparation\": \"groenten aan stoven , tomatenpuree toevoegen , bevochtigen met de gepelde tomaat en water. - als de groenten gaar zijn mixen. - pasta opstomen en mengen met de tomatensaus , overstrooien met de olijven ,paprikareepjes en feta, verkruimel deze over de pasta alternatief: met fijne boontjes en sojaolie\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5787,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5787,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5787,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5787,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5787,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5787,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 5787,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7350,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9520,\n          \"menuItemId\": 7350,\n          \"courseId\": 1451,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1451,\n            \"dispNameNl\": \"Luikse sla \",\n            \"dispNameEn\": \"Salade li\\u00e9geoise \",\n            \"nameNl\": \"luikse sla dd, z - MINDER VLEES\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": \"40 gr spek per persoon\",\n            \"preparation\": \"boontjes en aardappelen beetgaar steamen . - aardappelen en spek aanbakken in vetstof. - de boontjes toevoegen en alles afkruiden. - gekookte eieren in partjes snijden en ermee onder doen - afwerken met tomatenschijfjes en peterseli. dressing maken met slasaus en mosterd (keuze om mosterd 1 liter of 3 liter te gebruiken)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1451,\n                \"courseLogoId\": 212\n              },\n              {\n                \"courseId\": 1451,\n                \"courseLogoId\": 216\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7483,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9703,\n          \"menuItemId\": 7483,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7503,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9724,\n          \"menuItemId\": 7503,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7523,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9744,\n          \"menuItemId\": 7523,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7543,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9764,\n          \"menuItemId\": 7543,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7654,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 9883,\n          \"menuItemId\": 7654,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7659,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 9888,\n          \"menuItemId\": 7659,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7664,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 9893,\n          \"menuItemId\": 7664,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7669,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9898,\n          \"menuItemId\": 7669,\n          \"courseId\": 2341,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2341,\n            \"dispNameNl\": \"Worstenbroodje\",\n            \"dispNameEn\": \"Sausage roll\",\n            \"nameNl\": \"worstenbroodje gevogelte\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"halal\",\n            \"preparation\": null,\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2341,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 2341,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7674,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9903,\n          \"menuItemId\": 7674,\n          \"courseId\": 4884,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4884,\n            \"dispNameNl\": \"Rauwkostbox\",\n            \"dispNameEn\": \"Crudit\\u00e9 box\",\n            \"nameNl\": \"00 rauwkostbox z&w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"200 gr\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"rauwkost verwerk de rest van je rauwkost dat in een rauwkostsalade.- voeg geen sauzen toe.- voeg geen opgelegd fruit toe.-\",\n            \"price\": 2.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 4884,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4884,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 4884,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7683,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 954,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9925,\n          \"menuItemId\": 7683,\n          \"courseId\": 5030,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5030,\n            \"dispNameNl\": \"Kaasfeuillet\\u00e9\",\n            \"dispNameEn\": \"Cheese puff pastry\",\n            \"nameNl\": \"00 kaasfeuillet\\u00e9\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5030,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5030,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5030,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5030,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_cgb.raw.json",
    "content": "{\n  \"id\": 983,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7575,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9808,\n          \"menuItemId\": 7575,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7592,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9823,\n          \"menuItemId\": 7592,\n          \"courseId\": 3866,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3866,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, w (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade winter 6 falafels per persoon- pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox gele wortel in fijne reepjes snijden (optioneel toevoegen) hzs: falafel in oven bakken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3866,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3866,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7581,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9813,\n          \"menuItemId\": 7581,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7598,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9829,\n          \"menuItemId\": 7598,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7605,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9835,\n          \"menuItemId\": 7605,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7611,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9967,\n          \"menuItemId\": 7611,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7617,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 9845,\n          \"menuItemId\": 7617,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7740,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9980,\n          \"menuItemId\": 7740,\n          \"courseId\": 1109,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1109,\n            \"dispNameNl\": \"Hamburger\",\n            \"dispNameEn\": \"Hamburger\",\n            \"nameNl\": \"hamburger standaard, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": \"keuze om ketchup 3 liter of 1 liter te gebruiken\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1109,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1109,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7744,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 983,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 9984,\n          \"menuItemId\": 7744,\n          \"courseId\": 1537,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1537,\n            \"dispNameNl\": \"Rijstpap\",\n            \"dispNameEn\": \"Rice pudding\",\n            \"nameNl\": \"rijstpap kant-en klaar\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"garnituur: bruine suiker\",\n            \"preparation\": \"potjes vullen met de kant- en klare rijstpap en bruine suiker erbij serveren\",\n            \"price\": 1.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1537,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1537,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_cmi.raw.json",
    "content": "{\n  \"id\": 905,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7050,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9466,\n          \"menuItemId\": 7050,\n          \"courseId\": 997,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 997,\n            \"dispNameNl\": \"puree\",\n            \"dispNameEn\": \"mashed potatoes\",\n            \"nameNl\": \"puree zelf bereid\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"aardappelen steamen. - in klopper tot puree verwerken met melk en margarine en afsmaken.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 997,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9464,\n          \"menuItemId\": 7050,\n          \"courseId\": 1052,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1052,\n            \"dispNameNl\": \"ratatouille\",\n            \"dispNameEn\": \"ratatouille\",\n            \"nameNl\": \"ratatouille\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1052,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9463,\n          \"menuItemId\": 7050,\n          \"courseId\": 1274,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1274,\n            \"dispNameNl\": \"Groentekrustie \",\n            \"dispNameEn\": \"Vegetarian krustie \",\n            \"nameNl\": \"burger groentekrustie dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"frituren\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1274,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1274,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1274,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"prijs\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"price\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7049,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9121,\n          \"menuItemId\": 7049,\n          \"courseId\": 997,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 997,\n            \"dispNameNl\": \"puree\",\n            \"dispNameEn\": \"mashed potatoes\",\n            \"nameNl\": \"puree zelf bereid\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"aardappelen steamen. - in klopper tot puree verwerken met melk en margarine en afsmaken.\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 997,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9120,\n          \"menuItemId\": 7049,\n          \"courseId\": 1052,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1052,\n            \"dispNameNl\": \"ratatouille\",\n            \"dispNameEn\": \"ratatouille\",\n            \"nameNl\": \"ratatouille\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1052,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9119,\n          \"menuItemId\": 7049,\n          \"courseId\": 1741,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1741,\n            \"dispNameNl\": \"Visbrochette (MSC)\",\n            \"dispNameEn\": \"Fish skewer (MSC)\",\n            \"nameNl\": \"visbrochette voorgebakken, msc,dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1741,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7051,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9126,\n          \"menuItemId\": 7051,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7693,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9932,\n          \"menuItemId\": 7693,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7703,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9941,\n          \"menuItemId\": 7703,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7708,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9946,\n          \"menuItemId\": 7708,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7713,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9951,\n          \"menuItemId\": 7713,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7718,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 9956,\n          \"menuItemId\": 7718,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7723,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 9961,\n          \"menuItemId\": 7723,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7729,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 905,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 9971,\n          \"menuItemId\": 7729,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_cmu.raw.json",
    "content": "{\n  \"id\": 1007,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7840,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1007,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 10087,\n          \"menuItemId\": 7840,\n          \"courseId\": 5820,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5820,\n            \"dispNameNl\": \"komida@Mutsaard is voorlopig gesloten, maar u kan wel terecht in komida@Stadscampus\",\n            \"dispNameEn\": \"\",\n            \"nameNl\": \"komida@Mutsaard is temporarily closed, but you can go to komida@Stadscampus\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_cst.raw.json",
    "content": "{\n  \"id\": 921,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7217,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9307,\n          \"menuItemId\": 7217,\n          \"courseId\": 5811,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5811,\n            \"dispNameNl\": \"Chili con carne met rijst, tortillachips en zure room \",\n            \"dispNameEn\": \"Chili con carne with rice, tortilla chips and sour cream \",\n            \"nameNl\": \"Chili con carne met rijst, tortillachips en zure room (Iziii)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5811,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5811,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5811,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5811,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 5811,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": false,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7216,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9909,\n          \"menuItemId\": 7216,\n          \"courseId\": 1486,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1486,\n            \"dispNameNl\": \"Gegratineerde groentemoussaka \",\n            \"dispNameEn\": \"Vegetable moussaka \",\n            \"nameNl\": \"gegratineerde groentemoussaka veggie, zvv, dd, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"stoom de courgetten en aubergines gedurende 15min en laat uitlekken. - stoom de aard.schijfjes 10 min. bereid de witte- en tomatensaus. - kruid de witte saus af met lookpoeder en de tomatensaus met medina, sambal , tijm en paprika gemalen - eminceer de ui en fruit aan en kruiden. - bouw op in een diepe gastronorm als volgt laag aardappelen,courgette,aubergine,ajuin, feta (verkruimel de feta),overgiet met witte saus,bestrooi met gemalen kaas. - bak af op 175 graden gedurende 45 min . - dien op met tomatensaus.\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1486,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1486,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1486,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1486,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1486,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7215,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9305,\n          \"menuItemId\": 7215,\n          \"courseId\": 990,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 990,\n            \"dispNameNl\": \"pasta\",\n            \"dispNameEn\": \"pasta\",\n            \"nameNl\": \"pasta\",\n            \"nameEn\": \"\",\n            \"weight\": \"150 - 200g pp\",\n            \"extra\": null,\n            \"preparation\": \"kook de pasta gaar in licht gezouten water, sojaolie toevoegen, - kooktijd is afhankelijk van de soort pasta\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 990,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 990,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": true,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9304,\n          \"menuItemId\": 7215,\n          \"courseId\": 1783,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1783,\n            \"dispNameNl\": \"gegrilde kip en chorizo\",\n            \"dispNameEn\": \"grilled chicken and chorizo\",\n            \"nameNl\": \"pastasaus met gegrilde kip en chorizo, z&w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"pasta koken. kippenreepjes aanbakken in olijfolie met kippenkruiden. chorizo in blokjes snijden. - ajuin aanstoven in olijfolie en look toevoegen. chorizo toevoegen laten stoven. blussen met rode wijn en tomatenblokjes. - afsmaken met peper zout rozemarijn thijm en oregano. - kip aan saus toevoegen. - pasta opwarmen tot een temperatuur boven 65\\u00b0c bereikt is. - pasta afwerken met saus, parmezaan en rucola (zomer)/ postelein (winter)\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1783,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1783,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1783,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1783,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1783,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1783,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7625,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9853,\n          \"menuItemId\": 7625,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7632,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9910,\n          \"menuItemId\": 7632,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7639,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9867,\n          \"menuItemId\": 7639,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7646,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 921,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9874,\n          \"menuItemId\": 7646,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-25_hzs.raw.json",
    "content": "{\n  \"id\": 1021,\n  \"menuDate\": \"2020-09-25T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8034,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 1021,\n      \"sortorder\": 10,\n      \"menuItemContents\": []\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_cde.raw.json",
    "content": "{\n  \"id\": 958,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7396,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9576,\n          \"menuItemId\": 7396,\n          \"courseId\": 989,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 989,\n            \"dispNameNl\": \"gekookte aardappelen \",\n            \"dispNameEn\": \"boiled potatoes \",\n            \"nameNl\": \"aardappel natuur\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9575,\n          \"menuItemId\": 7396,\n          \"courseId\": 1033,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1033,\n            \"dispNameNl\": \"erwten en wortelen\",\n            \"dispNameEn\": \"peas and carrots\",\n            \"nameNl\": \"erwten en wortelen\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1033,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9574,\n          \"menuItemId\": 7396,\n          \"courseId\": 1393,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1393,\n            \"dispNameNl\": \"Varkenslapje\",\n            \"dispNameEn\": \"Pork escalope\",\n            \"nameNl\": \"varkenslapje dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"140g\",\n            \"extra\": null,\n            \"preparation\": \"voorbakken in braadpan - kruiden. - afbakken in oven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1393,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7389,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9565,\n          \"menuItemId\": 7389,\n          \"courseId\": 980,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 980,\n            \"dispNameNl\": \"frieten\",\n            \"dispNameEn\": \"fries\",\n            \"nameNl\": \"frieten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": \"afbakken in het frituur op 170 graden\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 980,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9564,\n          \"menuItemId\": 7389,\n          \"courseId\": 4930,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4930,\n            \"dispNameNl\": \"Kibbeling met tartaarsaus\",\n            \"dispNameEn\": \"Deep-fried fish with tartar sauce\",\n            \"nameNl\": \"kibbeling met tartaarsaus\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": \"foodmarket\",\n            \"preparation\": null,\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4930,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4930,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4930,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7383,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9556,\n          \"menuItemId\": 7383,\n          \"courseId\": 1562,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1562,\n            \"dispNameNl\": \"Pasta met schorseneren in lookboter met ei, olijven en peultjes\",\n            \"dispNameEn\": \"salsify in garlic butter, with egg, olives and snow peas\",\n            \"nameNl\": \"schorseneren in lookboter, ei, olijven en peultjes, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"beetgare pasta voorzichtig mengen met voorgesteamde peultjes en schorseneren, grof gesneden eitjes, look, olijven, peterselie en gesmolten boter - afkruiden met pezo. - in gn van 6 diep verdelen en opwarmen in combi-steamer. scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton gebruiken\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1562,\n                \"allergenId\": 200\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1562,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1562,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7484,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9704,\n          \"menuItemId\": 7484,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7504,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9725,\n          \"menuItemId\": 7504,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7524,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9745,\n          \"menuItemId\": 7524,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7544,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9765,\n          \"menuItemId\": 7544,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7876,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 10115,\n          \"menuItemId\": 7876,\n          \"courseId\": 2341,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2341,\n            \"dispNameNl\": \"Worstenbroodje\",\n            \"dispNameEn\": \"Sausage roll\",\n            \"nameNl\": \"worstenbroodje gevogelte\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"halal\",\n            \"preparation\": null,\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2341,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 2341,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7881,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 10120,\n          \"menuItemId\": 7881,\n          \"courseId\": 5030,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5030,\n            \"dispNameNl\": \"Kaasfeuillet\\u00e9\",\n            \"dispNameEn\": \"Cheese puff pastry\",\n            \"nameNl\": \"00 kaasfeuillet\\u00e9\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5030,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5030,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5030,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5030,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7887,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 10125,\n          \"menuItemId\": 7887,\n          \"courseId\": 1152,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1152,\n            \"dispNameNl\": \"Chocolademuffin\",\n            \"dispNameEn\": \"Chocolate muffin\",\n            \"nameNl\": \"muffin chocolade\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 1.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1152,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1152,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1152,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1152,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7892,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 958,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 10130,\n          \"menuItemId\": 7892,\n          \"courseId\": 1552,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1552,\n            \"dispNameNl\": \"Crunchy yoghurt\",\n            \"dispNameEn\": \"Crunchy yoghurt\",\n            \"nameNl\": \"crunchy yoghurt\",\n            \"nameEn\": \"\",\n            \"weight\": \"300ml\",\n            \"extra\": null,\n            \"preparation\": \"potjes vullen met krieken en bijvullen met yoghurt - dekseltje vullen met crunchy en op potje zetten keuze om volle of magere yoghurt te gebruiken keuze uit krieken bereid uit blik of krieken uit bokaal (wat je in stock heb)\",\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1552,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_cgb.raw.json",
    "content": "{\n  \"id\": 1011,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7915,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 10159,\n          \"menuItemId\": 7915,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7920,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 10164,\n          \"menuItemId\": 7920,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7910,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 10154,\n          \"menuItemId\": 7910,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7925,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 10169,\n          \"menuItemId\": 7925,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7930,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 10174,\n          \"menuItemId\": 7930,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7935,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 10179,\n          \"menuItemId\": 7935,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7940,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 10184,\n          \"menuItemId\": 7940,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7946,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 10191,\n          \"menuItemId\": 7946,\n          \"courseId\": 1081,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1081,\n            \"dispNameNl\": \"Croque monsieur \",\n            \"dispNameEn\": \"Croque monsieur \",\n            \"nameNl\": \"croque monsieur standaard, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"keuze uit wit brood of bruin brood om de croque te maken 1 potje saus is in de prijs inbegrepen\",\n            \"price\": 1.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1081,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1081,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1081,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1081,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1081,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 1081,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7951,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1011,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 10196,\n          \"menuItemId\": 7951,\n          \"courseId\": 1552,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1552,\n            \"dispNameNl\": \"Crunchy yoghurt\",\n            \"dispNameEn\": \"Crunchy yoghurt\",\n            \"nameNl\": \"crunchy yoghurt\",\n            \"nameEn\": \"\",\n            \"weight\": \"300ml\",\n            \"extra\": null,\n            \"preparation\": \"potjes vullen met krieken en bijvullen met yoghurt - dekseltje vullen met crunchy en op potje zetten keuze om volle of magere yoghurt te gebruiken keuze uit krieken bereid uit blik of krieken uit bokaal (wat je in stock heb)\",\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1552,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_cmi.raw.json",
    "content": "{\n  \"id\": 906,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7324,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9477,\n          \"menuItemId\": 7324,\n          \"courseId\": 911,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 911,\n            \"dispNameNl\": \"currysaus\",\n            \"dispNameEn\": \"curry sauce\",\n            \"nameNl\": \"currysaus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"ajuin aanstoven-bestrooien met curry, bloem en appelmoes - drogen - water toevoegen + kokosmelk - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux. - afsmaken met pezo\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 911,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 911,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 911,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9476,\n          \"menuItemId\": 7324,\n          \"courseId\": 999,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 999,\n            \"dispNameNl\": \"rijst\",\n            \"dispNameEn\": \"rice\",\n            \"nameNl\": \"rijst\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"kook de rijst gaar in licht gezouten water -\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9475,\n          \"menuItemId\": 7324,\n          \"courseId\": 1320,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1320,\n            \"dispNameNl\": \"Groenteloempia\",\n            \"dispNameEn\": \"Vegetable spring roll\",\n            \"nameNl\": \"groenteloempia, zvv, dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"2 stuks per persoon\",\n            \"extra\": null,\n            \"preparation\": \"loemia per 20 op platte gastronorm schikken . - oven voorverwarmen op 180\\u00b0. - per 10 stuks op gastroplaten schikken en koelen - loempia verwarmen in de oven tot een temperatuur van minstens 65\\u00b0c bereikt is\",\n            \"price\": 4.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1320,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1320,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1320,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1320,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 10136,\n          \"menuItemId\": 7324,\n          \"courseId\": 5827,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5827,\n            \"dispNameNl\": \"Gebakken sojascheuten\",\n            \"dispNameEn\": \"Fried soy sprouts\",\n            \"nameNl\": \"Gebakken sojascheuten\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5827,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7067,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9474,\n          \"menuItemId\": 7067,\n          \"courseId\": 911,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 911,\n            \"dispNameNl\": \"currysaus\",\n            \"dispNameEn\": \"curry sauce\",\n            \"nameNl\": \"currysaus\",\n            \"nameEn\": \"\",\n            \"weight\": \"50 ml\",\n            \"extra\": null,\n            \"preparation\": \"ajuin aanstoven-bestrooien met curry, bloem en appelmoes - drogen - water toevoegen + kokosmelk - 1 uur laten koken op klein vuur - mixen en afbinden met blanke roux. - afsmaken met pezo\",\n            \"price\": 0.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 911,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 911,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 911,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9473,\n          \"menuItemId\": 7067,\n          \"courseId\": 999,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 999,\n            \"dispNameNl\": \"rijst\",\n            \"dispNameEn\": \"rice\",\n            \"nameNl\": \"rijst\",\n            \"nameEn\": \"\",\n            \"weight\": \"150g\",\n            \"extra\": null,\n            \"preparation\": \"kook de rijst gaar in licht gezouten water -\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 9472,\n          \"menuItemId\": 7067,\n          \"courseId\": 1381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1381,\n            \"dispNameNl\": \"Loempia met kip\",\n            \"dispNameEn\": \"Chicken spring roll\",\n            \"nameNl\": \"loempia met kip1 dd - MINDER VLEES\",\n            \"nameEn\": \"\",\n            \"weight\": \"170g\",\n            \"extra\": null,\n            \"preparation\": \"laten ontdooien - 10 minuten voorbakken (op plaatjes met bakpapier) in oven van 180\\u00b0c afbakken in frituur\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1381,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1381,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1381,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1381,\n                \"courseLogoId\": 216\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 10137,\n          \"menuItemId\": 7067,\n          \"courseId\": 5827,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5827,\n            \"dispNameNl\": \"Gebakken sojascheuten\",\n            \"dispNameEn\": \"Fried soy sprouts\",\n            \"nameNl\": \"Gebakken sojascheuten\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5827,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7898,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 10138,\n          \"menuItemId\": 7898,\n          \"courseId\": 5828,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5828,\n            \"dispNameNl\": \"Pasta met kervelroom, jonge spinazie, zongedroogde tomaten en pompoenpitten\",\n            \"dispNameEn\": \"Pasta with chervil cream, baby spinach, sun-dried tomatoes and pine nuts\",\n            \"nameNl\": \"Pasta met kervelroom, jonge spinazie, zongedroogde tomaten en pompoenpitten\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"voorgegaarde pasta mengen met zongedroogde tomaten, pezo,sojaolie, gember,pesto en sambal oelek. - spinazie en venkel bakken en toevoegen aan de pasta - in gastro's 6 cm scheppen - opwarmen en afwerken met kervel en pompoenpitten.- kervelsaus maken (zie receptuur). - kervelsaus en gemalen kaas apart bijgeven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5828,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5828,\n                \"allergenId\": 206\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5828,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 5828,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7899,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 10139,\n          \"menuItemId\": 7899,\n          \"courseId\": 1561,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1561,\n            \"dispNameNl\": \"Pasta met kervel, gerookte zalm en zongedroogde tomaten\",\n            \"dispNameEn\": \"chervil, smoked salmon and sun-dried tomatoes\",\n            \"nameNl\": \"kervel, gerookte zalm en zongedroogde tomaten\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"beetgare pasta mengen met arachideolie, zongedroogde tomaten, lookpasta en zalmsnippers. - afkruiden met pezo en pa\\u00eblla mix . in gn van 6 diep verdelen en opwarmen in combi-steamer. - kervel wassen en bovenste gedeelte fijn snijden en over warme pasta strooien samen met de pompoenpitten ; - steeltjes van de kervel met de witte basissaus mengen en mixen en bij gerecht serveren\",\n            \"price\": 4.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1561,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1561,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1561,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1561,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1561,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1561,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7997,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 10253,\n          \"menuItemId\": 7997,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8003,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 10258,\n          \"menuItemId\": 8003,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8009,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 10263,\n          \"menuItemId\": 8009,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8014,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 7,\n      \"menuItemContents\": [\n        {\n          \"id\": 10268,\n          \"menuItemId\": 8014,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8019,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 8,\n      \"menuItemContents\": [\n        {\n          \"id\": 10273,\n          \"menuItemId\": 8019,\n          \"courseId\": 661,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 661,\n            \"dispNameNl\": \"Brie-appelbagnat \",\n            \"dispNameEn\": \"Pan bagnat with brie and apple \",\n            \"nameNl\": \"brie - appel bagnat (brie, appel, noten) dd, z (veggie)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"walnoten in stukken hakken indien nodig - citroensap voor bij de appeltjes\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 661,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 661,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 661,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8024,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 9,\n      \"menuItemContents\": [\n        {\n          \"id\": 10278,\n          \"menuItemId\": 8024,\n          \"courseId\": 550,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 550,\n            \"dispNameNl\": \"Kalkoenfinesse met zomergroenten\",\n            \"dispNameEn\": \"Turkey finesse with summer vegetables\",\n            \"nameNl\": \"kalkoenfinesse met zomergroentjes dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"scharrelei gekookt rico 75st of scharrelei gekookt rico 6x12st karton\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 550,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 550,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 550,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8050,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 906,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 10304,\n          \"menuItemId\": 8050,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_cmu.raw.json",
    "content": "{\n  \"id\": 1027,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 5,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8040,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1027,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 10294,\n          \"menuItemId\": 8040,\n          \"courseId\": 5820,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5820,\n            \"dispNameNl\": \"komida@Mutsaard is voorlopig gesloten\",\n            \"dispNameEn\": \"\",\n            \"nameNl\": \"komida@Mutsaard is temporarily closed\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_cst.parsed.expected.yaml",
    "content": "$test_case:\n  course_of_interest: 7221\n  reason: |\n    This response originally broke an assumption that a course component that is neither deleted nor enabled should not\n    show. The official site however does show these courses so we should as well.\ncampus: cst\ndate: '2020-09-28'\nmenu:\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - PASTA\n    - PIG\n    - VEAL\n    name:\n      en: Pasta bolognese sauce\n      nl: Pasta bolognaise\n  external_id: 7219\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 2\n- components:\n  - allergens:\n    - CELERY\n    - MILK_LACTOSE\n    attributes:\n    - CHEESE\n    - VEGGIE\n    name:\n      en: Mexican loaded sweet potato with cheddar cheese and nachos\n      nl: Mexicaans gevulde zoete aardappel met cheddar en nachos\n  external_id: 7220\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 1\n- components:\n  - allergens:\n    - FISH\n    - MILK_LACTOSE\n    - NUTS\n    - PEANUTS\n    - SHELLFISH\n    - SOY\n    attributes:\n    - CHICKEN\n    name:\n      en: Chicken tikka masala with rice and salsa (iziii)\n      nl: Kip tikka masala met grove salsa & rijst (iziii)\n    $test_case:\n      comment: This item has both enabled and deleted set to false\n  external_id: 7221\n  multiple_prices: true\n  price: '4.80'\n  sort_order: 0\n- components:\n  - allergens:\n    - NUTS\n    - SESAME\n    - SHELLFISH\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - FISH\n    - SALAD\n    name:\n      en: Buddha bowl with scampi\n      nl: Buddha bowl met scampi's\n  external_id: 7626\n  multiple_prices: true\n  price: '5.00'\n  sort_order: 3\n- components:\n  - allergens:\n    - CELERY\n    - SESAME\n    - WHEAT_GLUTEN\n    attributes:\n    - SALAD\n    - VEGAN\n    name:\n      en: Falafel and hummus salad\n      nl: Salade met falafel en humus\n  external_id: 7633\n  multiple_prices: true\n  price: '3.80'\n  sort_order: 4\n- components:\n  - allergens:\n    - MILK_LACTOSE\n    - NUTS\n    - SESAME\n    - SOY\n    - SULFITES\n    - WHEAT_GLUTEN\n    attributes:\n    - CHEESE\n    - SALAD\n    - VEGGIE\n    name:\n      en: \"\\u2018Classic\\u2019 tomato mozzarella\"\n      nl: Tomaat-mozzarella 'classic'\n  external_id: 7640\n  multiple_prices: true\n  price: '4.20'\n  sort_order: 5\n- components:\n  - allergens:\n    - CELERY\n    - EGG\n    - MILK_LACTOSE\n    - MUSTARD\n    - NUTS\n    - PEANUTS\n    - SOY\n    - WHEAT_GLUTEN\n    attributes:\n    - CHICKEN\n    - PIG\n    - SALAD\n    name:\n      en: Summer salad with chicken, bacon strips and avocado\n      nl: Zomerse salade met kip, spekjes en avocado\n  external_id: 7647\n  multiple_prices: true\n  price: '4.40'\n  sort_order: 6\n"
  },
  {
    "path": "tests/external_menus/2020-09-28_cst.processed.expected.yaml",
    "content": "campus: cst\ndate: '2020-09-28'\nmenu:\n- course_allergens:\n  - CELERY\n  - MILK_LACTOSE\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - PASTA\n  - PIG\n  - VEAL\n  course_sub_type: NORMAL\n  course_type: PASTA\n  external_id: 7219\n  name:\n    en: Pasta bolognese sauce\n    nl: Pasta bolognaise\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - CELERY\n  - MILK_LACTOSE\n  course_attributes:\n  - CHEESE\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: DAILY\n  external_id: 7220\n  name:\n    en: Mexican loaded sweet potato with cheddar cheese and nachos\n    nl: Mexicaans gevulde zoete aardappel met cheddar en nachos\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - FISH\n  - MILK_LACTOSE\n  - NUTS\n  - PEANUTS\n  - SHELLFISH\n  - SOY\n  course_attributes:\n  - CHICKEN\n  course_sub_type: NORMAL\n  course_type: DAILY\n  external_id: 7221\n  name:\n    en: Chicken tikka masala with rice and salsa (iziii)\n    nl: Kip tikka masala met grove salsa & rijst (iziii)\n  price_staff: '6.00'\n  price_students: '4.80'\n- course_allergens:\n  - NUTS\n  - SESAME\n  - SHELLFISH\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - FISH\n  - SALAD\n  course_sub_type: NORMAL\n  course_type: SALAD\n  external_id: 7626\n  name:\n    en: Buddha bowl with scampi\n    nl: Buddha bowl met scampi's\n  price_staff: '6.20'\n  price_students: '5.00'\n- course_allergens:\n  - CELERY\n  - SESAME\n  - WHEAT_GLUTEN\n  course_attributes:\n  - SALAD\n  - VEGAN\n  course_sub_type: VEGAN\n  course_type: SALAD\n  external_id: 7633\n  name:\n    en: Falafel and hummus salad\n    nl: Salade met falafel en humus\n  price_staff: '4.70'\n  price_students: '3.80'\n- course_allergens:\n  - MILK_LACTOSE\n  - NUTS\n  - SESAME\n  - SOY\n  - SULFITES\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHEESE\n  - SALAD\n  - VEGGIE\n  course_sub_type: VEGETARIAN\n  course_type: SALAD\n  external_id: 7640\n  name:\n    en: \"\\u2018Classic\\u2019 tomato mozzarella\"\n    nl: Tomaat-mozzarella 'classic'\n  price_staff: '5.20'\n  price_students: '4.20'\n- course_allergens:\n  - CELERY\n  - EGG\n  - MILK_LACTOSE\n  - MUSTARD\n  - NUTS\n  - PEANUTS\n  - SOY\n  - WHEAT_GLUTEN\n  course_attributes:\n  - CHICKEN\n  - PIG\n  - SALAD\n  course_sub_type: NORMAL\n  course_type: SALAD\n  external_id: 7647\n  name:\n    en: Summer salad with chicken, bacon strips and avocado\n    nl: Zomerse salade met kip, spekjes en avocado\n  price_staff: '5.50'\n  price_students: '4.40'\n"
  },
  {
    "path": "tests/external_menus/2020-09-28_cst.raw.json",
    "content": "{\n  \"id\": 922,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 7221,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 9312,\n          \"menuItemId\": 7221,\n          \"courseId\": 5790,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5790,\n            \"dispNameNl\": \"Kip tikka masala met grove salsa & rijst (iziii)\",\n            \"dispNameEn\": \"Chicken tikka masala with rice and salsa (iziii)\",\n            \"nameNl\": \"Kip tikka masala met grove salsa & rijst (iziii)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"Stoom de broccoli. - Marineer de kip in de yoghurt tandoorikruiden en garam masala. - ontdoe de kip van de meeste marinade. - verhit de olie en kleur de kip kort. Verwijder de kip en voeg de ui en look toe. - blus met de tomatenblokjes., voeg de currypasta toe en smaak af met pezo,sambal, gember en citroensap. - voeg de kip toe en laat zachtjes garen. - werk af met kokosmelk,koriander en de gestoomde broccoli. Serveer met de rijst en de grove salsa.\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5790,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5790,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": false,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7220,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 9311,\n          \"menuItemId\": 7220,\n          \"courseId\": 3461,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3461,\n            \"dispNameNl\": \"Mexicaans gevulde zoete aardappel met cheddar en nachos\",\n            \"dispNameEn\": \"Mexican loaded sweet potato with cheddar cheese and nachos\",\n            \"nameNl\": \"mexicaans gevulde zoete aardappel met cheddar en nachos, dd, zvv, z & w\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": null,\n            \"preparation\": \"1. kuis en spoel de zoete aardappelen grondig en schil ze (indien nodig). stoom deze voor 3/4de gaar. 2. stoof de sjalot, bleekselder, paprikablokjes gaar en voeg de rode bonen en mais toe op het laatste (2.5kg gebruiken van de mais). 3. kruid met pezo, komijn en cayennepeper. 3. halveer de zoete aardappelen / indien dit niet lukt (verschillende groottes) snij dan de zoete aardappelen in plakjes. je mag ze ook deels uithollen, het vulsel mag dan mee vermengd worden met de andere groentjes. 4. stabiliseer de zoete aardappelen in aan gastronorm en doe de groentjes erover. 5. strooi er de cheddar kaas over en laat in oven nog even gratineren. 6. serveer met peterselie en zure room. 7. salade maken (veldsla (winter) /babyleaf (zomer), wortel, rode kool en mais) en afwerken met olijfolie en pezo en tortillachips bijgeven\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3461,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3461,\n                \"allergenId\": 208\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3461,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3461,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"prijs\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"price\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7219,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 9309,\n          \"menuItemId\": 7219,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7626,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 3,\n      \"menuItemContents\": [\n        {\n          \"id\": 9854,\n          \"menuItemId\": 7626,\n          \"courseId\": 5225,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5225,\n            \"dispNameNl\": \"Buddha bowl met scampi's\",\n            \"dispNameEn\": \"Buddha bowl with scampi\",\n            \"nameNl\": \"00 buddha bowl met scampi's, zomer\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rijst klaar volgens de bereidingswijze op de verpakking, roer de rijstazijn door de rijst als die nog warm is en laat de rijst afkoelen . - stoof de paksoi & breng op smaak met pezo. - maak de scampi's klaar & laat afkoelen.- maak de bowl: doe de rijst in een bowl, maak een rijtje edamame, een rijtje schijfjes wortelen en radijsjes, een rijtje babyleaf, schijfjes avocado (besprenkelt met citroensap) en rijtje paksoi. werk af met de scampi's, koriander en gesneden pijpajuin. serveer hier bij de sojavinaigrette.\",\n            \"price\": 5.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5225,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7633,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 4,\n      \"menuItemContents\": [\n        {\n          \"id\": 9911,\n          \"menuItemId\": 7633,\n          \"courseId\": 3865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3865,\n            \"dispNameNl\": \"Salade met falafel en humus\",\n            \"dispNameEn\": \"Falafel and hummus salad\",\n            \"nameNl\": \"falafel salade met humus, z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - vegan\",\n            \"preparation\": \"conceptsalade zomer 6 falafels per persoon -pitabroodjes in 4 snijden - 2 stukjes pitabrood mee in saladebox falafel in oven bakken voor hzs\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3865,\n                \"allergenId\": 209\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3865,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7640,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 5,\n      \"menuItemContents\": [\n        {\n          \"id\": 9868,\n          \"menuItemId\": 7640,\n          \"courseId\": 3872,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3872,\n            \"dispNameNl\": \"Tomaat-mozzarella 'classic'\",\n            \"dispNameEn\": \"\\u2018Classic\\u2019 tomato mozzarella\",\n            \"nameNl\": \"tomaat-mozzarella \\\"classic\\\", z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"mozzarella kruiden met pezo - mozzarella onderaan - dan tomaten - dan basilicum - zonnebloempitten en romeinse sla bovenaan - dressing in dressingspotje\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 3872,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 3872,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 7647,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 922,\n      \"sortorder\": 6,\n      \"menuItemContents\": [\n        {\n          \"id\": 9875,\n          \"menuItemId\": 7647,\n          \"courseId\": 5286,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5286,\n            \"dispNameNl\": \"Zomerse salade met kip, spekjes en avocado\",\n            \"dispNameEn\": \"Summer salad with chicken, bacon strips and avocado\",\n            \"nameNl\": \"00 zomerse salade met kip, spekjes & avocado,z\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"bak de kip met de kippenkruiden - bak de spekjes - snij de avocado in partjes en besprenkel ze met citroensap; werk in laagjes: kip, spekjes, avocado, gesneden ijsbergsla en kerstomaten - werk af met hennepzaad, platte peterselie, partjes citroen en een potje dressing\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5286,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5286,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-09-28_hzs.raw.json",
    "content": "{\n  \"id\": 1022,\n  \"menuDate\": \"2020-09-28T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8035,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1022,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 10288,\n          \"menuItemId\": 8035,\n          \"courseId\": 5829,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5829,\n            \"dispNameNl\": \"komida@Hogere Zeevaartschool is momenteel enkel het ophaalpunt voor de online bestellingen: https://nl-20202040ra.iziii.pro/\",\n            \"dispNameEn\": \"komida@Hogere Zeevaarschool functions as our pick up point for online orders: https://nl-20202040ra.iziii.pro/\",\n            \"nameNl\": \"komida@Hogere Zeevaartschool is momenteel enkel het ophaalpunt voor de online bestellingen: https://nl-20202040ra.iziii.pro/\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-10-26_cde.raw.json",
    "content": "{\n  \"id\": 1072,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 2,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8760,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 11148,\n          \"menuItemId\": 8760,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8410,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 10718,\n          \"menuItemId\": 8410,\n          \"courseId\": 2323,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2323,\n            \"dispNameNl\": \"Bladerdeeg met geitenkaas, rauwkostsalade en frietjes\",\n            \"dispNameEn\": \"xx Goat cheese puff pastry with crucit\\u00e9s and French fries\",\n            \"nameNl\": \"bladerdeeg geitenkaas (rauwkostsalade + frietjes), zvv, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": \"veggie\",\n            \"preparation\": \"bladerdeegjes openleggen op platte plaatjes met boterpapier. - afbakken op 180 \\u00b0c. + zomerslaatje erbij serveren (zie lijst zomer rauwkostsalades)\",\n            \"price\": 4.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 2323,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2323,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 2323,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9108,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 11504,\n          \"menuItemId\": 9108,\n          \"courseId\": 1412,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1412,\n            \"dispNameNl\": \"Pasta bolognaise\",\n            \"dispNameEn\": \"Pasta bolognese sauce\",\n            \"nameNl\": \"Pasta bolognaise saus\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"gehakt en ajuin aanbakken en alles een kookketel doen met water, groenten gelei bouillon, paprikareepjes ,knolselderblokjes, paprikareepjes, tomatenpuree, lookpasta, laten koken en afkruiden. - voor een half uur en binden met de bruine roux en fond bruin. alternatief: tomatenblokjes dv, sambal en tomatino\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1412,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 207\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1412,\n                \"courseLogoId\": 212\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8679,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11056,\n          \"menuItemId\": 8679,\n          \"courseId\": 5226,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5226,\n            \"dispNameNl\": \"Regenboog quinoa bowl\",\n            \"dispNameEn\": \"Rainbow quinoa bowl\",\n            \"nameNl\": \"00 regenboog quinoa bowl,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rode quinoa klaar volgens de bereidingswijze op de verpakking en laat afkoelen.- maak de humusdressing met sojayoghurt en humus, breng op smaak met peper en zout. - maak de bowl: doe de quinoa in een kom, vervolgens baby leaf, rode en groene paprikareepjes, reepjes komkommer, kerstomaten en kikkererwten.- werk af met de prei scheuten, granaatappelpitjes, sesamzaad en de tahini dressing\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8689,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11066,\n          \"menuItemId\": 8689,\n          \"courseId\": 3488,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3488,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - lenteui schuin versnijden, mengen met sojascheuten en paprikablokjes en kokoschilfers- opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 8709,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11086,\n          \"menuItemId\": 8709,\n          \"courseId\": 1865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1865,\n            \"dispNameNl\": \"Salade Dolce Vita\",\n            \"dispNameEn\": \"Dolce Vita salad\",\n            \"nameNl\": \"salade dolce vita (penne, mozzarella, courgette), dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"meer dan de helft van de salade moet bestaan uit groenten. courgette en aubergine snijden en kort roosteren.pasta koken. - kerstomaat en notensla wassen - olijven laten uitlekken. groenten mengen en afsmaken.- pasta groenten en mozzarella assembleren afwerken met notensla en hennepzaad\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9128,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11538,\n          \"menuItemId\": 9128,\n          \"courseId\": 525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 525,\n            \"dispNameNl\": \"Caesar salad on a bun\",\n            \"dispNameEn\": \"Caesar salad on a bun\",\n            \"nameNl\": \"caesar salad on a bun, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 525,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 525,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9135,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11545,\n          \"menuItemId\": 9135,\n          \"courseId\": 4169,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4169,\n            \"dispNameNl\": \"Nordic cottage cheese\",\n            \"dispNameEn\": \"Nordic cottage cheese\",\n            \"nameNl\": \"cottage cheese nordic (ger. zalm)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9138,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11548,\n          \"menuItemId\": 9138,\n          \"courseId\": 1552,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1552,\n            \"dispNameNl\": \"Crunchy yoghurt\",\n            \"dispNameEn\": \"Crunchy yoghurt\",\n            \"nameNl\": \"crunchy yoghurt\",\n            \"nameEn\": \"\",\n            \"weight\": \"300ml\",\n            \"extra\": null,\n            \"preparation\": \"potjes vullen met krieken en bijvullen met yoghurt - dekseltje vullen met crunchy en op potje zetten keuze om volle of magere yoghurt te gebruiken keuze uit krieken bereid uit blik of krieken uit bokaal (wat je in stock heb)\",\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1552,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1552,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9141,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11551,\n          \"menuItemId\": 9141,\n          \"courseId\": 1532,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1532,\n            \"dispNameNl\": \"Panna cotta\",\n            \"dispNameEn\": \"Panna cotta\",\n            \"nameNl\": \"panna cotta\",\n            \"nameEn\": \"\",\n            \"weight\": \"160g\",\n            \"extra\": \"garnituur: rote grutze/speculoos\",\n            \"preparation\": \"zie verpakking - speculoos of rote grutze er bovenop serveren\",\n            \"price\": 1.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1532,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1532,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9200,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11617,\n          \"menuItemId\": 9200,\n          \"courseId\": 3283,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3283,\n            \"dispNameNl\": \"New York cheesecake\",\n            \"dispNameEn\": \"New York cheese cake\",\n            \"nameNl\": \"new york cheesecake\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 1.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3283,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9206,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1072,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11623,\n          \"menuItemId\": 9206,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-10-26_cgb.raw.json",
    "content": "{\n  \"id\": 1136,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 4,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 9182,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 11592,\n          \"menuItemId\": 9182,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9192,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 10,\n      \"menuItemContents\": [\n        {\n          \"id\": 11603,\n          \"menuItemId\": 9192,\n          \"courseId\": 5548,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5548,\n            \"dispNameNl\": \"Carrot cake\",\n            \"dispNameEn\": \"carrot cake\",\n            \"nameNl\": \"Carrot cake\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 2.3,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5548,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5548,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5548,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5548,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5548,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9146,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11556,\n          \"menuItemId\": 9146,\n          \"courseId\": 530,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 530,\n            \"dispNameNl\": \"Tuinbroodje \",\n            \"dispNameEn\": \"Spring sandwich \",\n            \"nameNl\": \"tuinbroodje dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 530,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 530,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9151,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11561,\n          \"menuItemId\": 9151,\n          \"courseId\": 525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 525,\n            \"dispNameNl\": \"Caesar salad on a bun\",\n            \"dispNameEn\": \"Caesar salad on a bun\",\n            \"nameNl\": \"caesar salad on a bun, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 525,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 525,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9156,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11566,\n          \"menuItemId\": 9156,\n          \"courseId\": 4169,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4169,\n            \"dispNameNl\": \"Nordic cottage cheese\",\n            \"dispNameEn\": \"Nordic cottage cheese\",\n            \"nameNl\": \"cottage cheese nordic (ger. zalm)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9161,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11571,\n          \"menuItemId\": 9161,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9166,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11576,\n          \"menuItemId\": 9166,\n          \"courseId\": 1865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1865,\n            \"dispNameNl\": \"Salade Dolce Vita\",\n            \"dispNameEn\": \"Dolce Vita salad\",\n            \"nameNl\": \"salade dolce vita (penne, mozzarella, courgette), dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"meer dan de helft van de salade moet bestaan uit groenten. courgette en aubergine snijden en kort roosteren.pasta koken. - kerstomaat en notensla wassen - olijven laten uitlekken. groenten mengen en afsmaken.- pasta groenten en mozzarella assembleren afwerken met notensla en hennepzaad\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9171,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11581,\n          \"menuItemId\": 9171,\n          \"courseId\": 5226,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5226,\n            \"dispNameNl\": \"Regenboog quinoa bowl\",\n            \"dispNameEn\": \"Rainbow quinoa bowl\",\n            \"nameNl\": \"00 regenboog quinoa bowl,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rode quinoa klaar volgens de bereidingswijze op de verpakking en laat afkoelen.- maak de humusdressing met sojayoghurt en humus, breng op smaak met peper en zout. - maak de bowl: doe de quinoa in een kom, vervolgens baby leaf, rode en groene paprikareepjes, reepjes komkommer, kerstomaten en kikkererwten.- werk af met de prei scheuten, granaatappelpitjes, sesamzaad en de tahini dressing\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9176,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11586,\n          \"menuItemId\": 9176,\n          \"courseId\": 3487,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3487,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, w\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - pastinaak grillen en mengen met de wortelen en sojascheuten en kokosschilfers - opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 3487,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3487,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9187,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1136,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11598,\n          \"menuItemId\": 9187,\n          \"courseId\": 1113,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1113,\n            \"dispNameNl\": \"Mexicano\",\n            \"dispNameEn\": \"Mexicano\",\n            \"nameNl\": \"mexicano, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": null,\n            \"preparation\": \"keuze om ketchup 3 liter of 1 liter te gebruiken\",\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1113,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1113,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1113,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1113,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1113,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1113,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 1113,\n                \"courseLogoId\": 208\n              },\n              {\n                \"courseId\": 1113,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-10-26_cmi.raw.json",
    "content": "{\n  \"id\": 1104,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 3,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 9233,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 11652,\n          \"menuItemId\": 9233,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9119,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 11524,\n          \"menuItemId\": 9119,\n          \"courseId\": 1451,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1451,\n            \"dispNameNl\": \"Luikse sla \",\n            \"dispNameEn\": \"Salade li\\u00e9geoise \",\n            \"nameNl\": \"luikse sla dd, z - MINDER VLEES\",\n            \"nameEn\": \"\",\n            \"weight\": \"400g\",\n            \"extra\": \"40 gr spek per persoon\",\n            \"preparation\": \"boontjes en aardappelen beetgaar steamen . - aardappelen en spek aanbakken in vetstof. - de boontjes toevoegen en alles afkruiden. - gekookte eieren in partjes snijden en ermee onder doen - afwerken met tomatenschijfjes en peterseli. dressing maken met slasaus en mosterd (keuze om mosterd 1 liter of 3 liter te gebruiken)\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1451,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1451,\n                \"courseLogoId\": 212\n              },\n              {\n                \"courseId\": 1451,\n                \"courseLogoId\": 216\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9308,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 2,\n      \"menuItemContents\": [\n        {\n          \"id\": 11732,\n          \"menuItemId\": 9308,\n          \"courseId\": 5865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5865,\n            \"dispNameNl\": \"Lauw prinsessenslaatje met quorn\",\n            \"dispNameEn\": \"Luke-warm green bean salad with quorn pieces \",\n            \"nameNl\": \"Lauw prinsessenslaatje met quorn (tijdelijk COVID)\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5865,\n                \"allergenId\": 204\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9262,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11687,\n          \"menuItemId\": 9262,\n          \"courseId\": 3488,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3488,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - lenteui schuin versnijden, mengen met sojascheuten en paprikablokjes en kokoschilfers- opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9267,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11693,\n          \"menuItemId\": 9267,\n          \"courseId\": 1865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1865,\n            \"dispNameNl\": \"Salade Dolce Vita\",\n            \"dispNameEn\": \"Dolce Vita salad\",\n            \"nameNl\": \"salade dolce vita (penne, mozzarella, courgette), dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"meer dan de helft van de salade moet bestaan uit groenten. courgette en aubergine snijden en kort roosteren.pasta koken. - kerstomaat en notensla wassen - olijven laten uitlekken. groenten mengen en afsmaken.- pasta groenten en mozzarella assembleren afwerken met notensla en hennepzaad\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9272,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11698,\n          \"menuItemId\": 9272,\n          \"courseId\": 5226,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5226,\n            \"dispNameNl\": \"Regenboog quinoa bowl\",\n            \"dispNameEn\": \"Rainbow quinoa bowl\",\n            \"nameNl\": \"00 regenboog quinoa bowl,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rode quinoa klaar volgens de bereidingswijze op de verpakking en laat afkoelen.- maak de humusdressing met sojayoghurt en humus, breng op smaak met peper en zout. - maak de bowl: doe de quinoa in een kom, vervolgens baby leaf, rode en groene paprikareepjes, reepjes komkommer, kerstomaten en kikkererwten.- werk af met de prei scheuten, granaatappelpitjes, sesamzaad en de tahini dressing\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9277,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11703,\n          \"menuItemId\": 9277,\n          \"courseId\": 5499,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5499,\n            \"dispNameNl\": \"Pepper rocket\",\n            \"dispNameEn\": \"pepper rocket\",\n            \"nameNl\": \"Pepper rocket\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"\",\n            \"preparation\": \"\",\n            \"price\": 3.4,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5499,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 5499,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9282,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11708,\n          \"menuItemId\": 9282,\n          \"courseId\": 525,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 525,\n            \"dispNameNl\": \"Caesar salad on a bun\",\n            \"dispNameEn\": \"Caesar salad on a bun\",\n            \"nameNl\": \"caesar salad on a bun, dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.6,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 525,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 525,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 525,\n                \"courseLogoId\": 210\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9287,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11713,\n          \"menuItemId\": 9287,\n          \"courseId\": 4169,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 4169,\n            \"dispNameNl\": \"Nordic cottage cheese\",\n            \"dispNameEn\": \"Nordic cottage cheese\",\n            \"nameNl\": \"cottage cheese nordic (ger. zalm)\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 4169,\n                \"courseLogoId\": 215\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9293,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 1104,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11718,\n          \"menuItemId\": 9293,\n          \"courseId\": 530,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 530,\n            \"dispNameNl\": \"Tuinbroodje \",\n            \"dispNameEn\": \"Spring sandwich \",\n            \"nameNl\": \"tuinbroodje dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"250g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": null,\n            \"price\": 3.1,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 530,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 530,\n                \"allergenId\": 203\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 210\n              },\n              {\n                \"courseId\": 530,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-10-26_cst.raw.json",
    "content": "{\n  \"id\": 932,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 1,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 8759,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 932,\n      \"sortorder\": 0,\n      \"menuItemContents\": [\n        {\n          \"id\": 11147,\n          \"menuItemId\": 8759,\n          \"courseId\": 2381,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 2381,\n            \"dispNameNl\": \"Tomatensoep\",\n            \"dispNameEn\": \"Tomato soup\",\n            \"nameNl\": \"tomatensoep\",\n            \"nameEn\": \"\",\n            \"weight\": \"500 ml / 700ml\",\n            \"extra\": \"opwarmen in microgolf op 750 watt - roeren en genieten maar! smakelijk!\",\n            \"preparation\": \"groenten en aardappelen in water aan de kook brengen met de bio groentebouillon. - daarna de tomatenpuree toevoegen - mixen en afsmaken.\",\n            \"price\": 1.5,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"allergenId\": 212\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 211\n              },\n              {\n                \"courseId\": 2381,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": \"klein: \\u20ac 0.90\",\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": true,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": \"large: 1.20\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9197,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 932,\n      \"sortorder\": 1,\n      \"menuItemContents\": [\n        {\n          \"id\": 11650,\n          \"menuItemId\": 9197,\n          \"courseId\": 978,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 978,\n            \"dispNameNl\": \"pommes Duchesse\",\n            \"dispNameEn\": \"pommes duchesse\",\n            \"nameNl\": \"aardappelen duchesse\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g pp\",\n            \"extra\": null,\n            \"preparation\": null,\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 978,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 978,\n                \"allergenId\": 201\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 11608,\n          \"menuItemId\": 9197,\n          \"courseId\": 1378,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1378,\n            \"dispNameNl\": \"Koninginnehapje\",\n            \"dispNameEn\": \"Chicken vol-au-vent\",\n            \"nameNl\": \"koninginnehapje dd\",\n            \"nameEn\": \"\",\n            \"weight\": \"200g\",\n            \"extra\": null,\n            \"preparation\": \"( zo kort mogelijk voor service bereiden ! ) kippenreepjes en balletjes opwarmen en samen met de gebakken ajuin en champignons mengen in diepe bak warme witte saus + bouillon van kip erover gieten en mengen tot juiste consistentie. - volledig afsmaken en afwerken met peterselie - + pasteitjes 5 min. opwarmen in oven\",\n            \"price\": 5.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1378,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1378,\n                \"courseLogoId\": 202\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        },\n        {\n          \"id\": 11612,\n          \"menuItemId\": 9197,\n          \"courseId\": 5514,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5514,\n            \"dispNameNl\": \"Rauwkostslaatje\",\n            \"dispNameEn\": \"crudit\\u00e9s\",\n            \"nameNl\": \"rauwkostslaatje\",\n            \"nameEn\": \"\",\n            \"weight\": \"\",\n            \"extra\": \"Dit is een slaatje om bij een gerecht toe te voegen, waarbij de salade al in de prijs gerekend is.\",\n            \"preparation\": \"\",\n            \"price\": 0.0,\n            \"photo\": \"\",\n            \"isCourse\": true,\n            \"isIngredient\": true,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 202\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 207\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 208\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 211\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 212\n              },\n              {\n                \"courseId\": 5514,\n                \"allergenId\": 213\n              }\n            ],\n            \"course_CourseLogos\": [],\n            \"maincourse\": false,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9212,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 932,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11629,\n          \"menuItemId\": 9212,\n          \"courseId\": 1865,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 1865,\n            \"dispNameNl\": \"Salade Dolce Vita\",\n            \"dispNameEn\": \"Dolce Vita salad\",\n            \"nameNl\": \"salade dolce vita (penne, mozzarella, courgette), dd, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"300g\",\n            \"extra\": \"koel bewaren - veggie\",\n            \"preparation\": \"meer dan de helft van de salade moet bestaan uit groenten. courgette en aubergine snijden en kort roosteren.pasta koken. - kerstomaat en notensla wassen - olijven laten uitlekken. groenten mengen en afsmaken.- pasta groenten en mozzarella assembleren afwerken met notensla en hennepzaad\",\n            \"price\": 4.2,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 203\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 206\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 210\n              },\n              {\n                \"courseId\": 1865,\n                \"allergenId\": 211\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 204\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 1865,\n                \"courseLogoId\": 214\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9217,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 932,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11634,\n          \"menuItemId\": 9217,\n          \"courseId\": 3488,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 3488,\n            \"dispNameNl\": \"Thai bombai salade met kip\",\n            \"dispNameEn\": \"Thai Bombai chicken salad\",\n            \"nameNl\": \"thai bombai kip salade, z\",\n            \"nameEn\": \"\",\n            \"weight\": \"350g\",\n            \"extra\": \"koel bewaren\",\n            \"preparation\": \"kip bakken in olijfolie met pezo - mie koken - mie mengen met de world grill saus - lenteui schuin versnijden, mengen met sojascheuten en paprikablokjes en kokoschilfers- opbouw: mie, kippenreepjes, groentjes en platte peterselie\",\n            \"price\": 4.4,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 200\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 3488,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 202\n              },\n              {\n                \"courseId\": 3488,\n                \"courseLogoId\": 209\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": false,\n            \"calculatedMultiplePrices\": true,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    },\n    {\n      \"id\": 9222,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 1,\n      \"remark\": null,\n      \"menuid\": 932,\n      \"sortorder\": 11,\n      \"menuItemContents\": [\n        {\n          \"id\": 11639,\n          \"menuItemId\": 9222,\n          \"courseId\": 5226,\n          \"sortOrder\": 0,\n          \"course\": {\n            \"id\": 5226,\n            \"dispNameNl\": \"Regenboog quinoa bowl\",\n            \"dispNameEn\": \"Rainbow quinoa bowl\",\n            \"nameNl\": \"00 regenboog quinoa bowl,z (vegan)\",\n            \"nameEn\": \"\",\n            \"weight\": null,\n            \"extra\": null,\n            \"preparation\": \"maak de rode quinoa klaar volgens de bereidingswijze op de verpakking en laat afkoelen.- maak de humusdressing met sojayoghurt en humus, breng op smaak met peper en zout. - maak de bowl: doe de quinoa in een kom, vervolgens baby leaf, rode en groene paprikareepjes, reepjes komkommer, kerstomaten en kikkererwten.- werk af met de prei scheuten, granaatappelpitjes, sesamzaad en de tahini dressing\",\n            \"price\": 3.8,\n            \"photo\": \"\",\n            \"isCourse\": false,\n            \"isIngredient\": false,\n            \"course_CategoryForCourses\": null,\n            \"course_Allergens\": [\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 201\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 205\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"allergenId\": 210\n              }\n            ],\n            \"course_CourseLogos\": [\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 209\n              },\n              {\n                \"courseId\": 5226,\n                \"courseLogoId\": 213\n              }\n            ],\n            \"maincourse\": true,\n            \"menuInfo\": null,\n            \"fixedMultiplePrices\": true,\n            \"calculatedMultiplePrices\": false,\n            \"fixedprice\": false,\n            \"showFirst\": false,\n            \"deleted\": false,\n            \"enabled\": true,\n            \"menuInfoEn\": null\n          }\n        }\n      ]\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/2020-10-26_hzs.raw.json",
    "content": "{\n  \"id\": 1155,\n  \"menuDate\": \"2020-10-26T00:00:00\",\n  \"restaurantId\": 6,\n  \"chefId\": 0,\n  \"description\": null,\n  \"approvedById\": 0,\n  \"approvedDateTime\": \"0001-01-01T00:00:00\",\n  \"approved\": false,\n  \"requestToBeApproved\": false,\n  \"remark\": null,\n  \"menuItems\": [\n    {\n      \"id\": 9261,\n      \"nameNl\": null,\n      \"nameEn\": null,\n      \"menuTypeId\": 0,\n      \"chefId\": 0,\n      \"enabled\": 0,\n      \"remark\": null,\n      \"menuid\": 1155,\n      \"sortorder\": 10,\n      \"menuItemContents\": []\n    }\n  ],\n  \"$schema\": \"./raw.schema.json\"\n}"
  },
  {
    "path": "tests/external_menus/download_external_jsons.py",
    "content": "import datetime\nimport json\nimport os\nimport sys\nimport time\nfrom collections import deque\nfrom datetime import datetime, timedelta\n\nimport requests\nfrom jsonschema import ValidationError, Draft7Validator\n\nBASE_ENDPOINT = 'https://restickets.uantwerpen.be/'\nMENU_API = '{endpoint}api/GetMenuByDate/{campus}/{date}'\n\nFILE_LOCATION = '{date}_{campus}.raw.json'\n\nAPI_GET_HEADERS = dict()\nAPI_GET_HEADERS['Accept'] = 'application/json'\n\ncampuses = {\n    'cst': 1,\n    'cde': 2,\n    'cmi': 3,\n    'cgb': 4,\n    'cmu': 5,\n    'hzs': 6,\n}\n\n\nclass Limiter:\n    def __init__(self, max_rate: int):\n        self.max_rate = max_rate\n        self.last_times = deque()\n\n    def __call__(self):\n        now = datetime.now()\n\n        if len(self.last_times) < self.max_rate:\n            self.last_times.append(now)\n            return\n\n        delta = (now - self.last_times.popleft()).total_seconds()\n\n        if delta < 1:\n            time.sleep(1.0 - delta)\n\n        self.last_times.append(now)\n\n\nif __name__ == '__main__':\n    if len(sys.argv) == 3:\n        do_requests = True\n    elif len(sys.argv) == 4:\n        do_requests = (sys.argv[3] != '0')\n    else:\n        print('Needs 2 parameters: first date and last date. Optionally 3rd parameter 0/1', file=sys.stderr)\n        sys.exit(1)\n\n    first = datetime.strptime(sys.argv[1], '%Y-%m-%d').date()\n    last = datetime.strptime(sys.argv[2], '%Y-%m-%d').date()\n\n    session = requests.Session()\n    limiter = Limiter(5)\n\n    with open('raw.schema.json') as f:\n        schema = json.load(f)\n\n    Draft7Validator.check_schema(schema)\n    validator = Draft7Validator(schema)\n\n    violations = []\n\n    for date in (first + timedelta(days=x) for x in range(0, (last - first).days + 1)):\n        date: datetime.date\n\n        if date.isoweekday() > 5:  # No weekends\n            continue\n\n        print('Date:', date, file=sys.stderr)\n\n        for campus, campus_id in campuses.items():\n            print('- Campus:', campus, file=sys.stderr)\n\n            url = MENU_API.format(endpoint=BASE_ENDPOINT, campus=campus_id, date=date.strftime('%Y-%m-%d'))\n            file = FILE_LOCATION.format(campus=campus, date=date.strftime('%Y-%m-%d'))\n\n            if os.path.isfile(file):\n                print('  Exists', file=sys.stderr)\n\n                try:\n                    with open(file, 'r') as f:\n                        data = json.load(f)\n                except json.decoder.JSONDecodeError:\n                    print('! Json decode error', file=sys.stderr)\n                    continue\n\n                try:\n                    validator.validate(data)\n                except ValidationError as e:\n                    print('! Schema validation failed: ', e, file=sys.stderr)\n\n                    violations.append((date, campus))\n            elif do_requests:\n                limiter()\n\n                response = session.get(url, headers=API_GET_HEADERS)\n                if 400 <= response.status_code < 500:\n                    print('  Client error on HTTP request', file=sys.stderr)\n                    continue\n                if 500 <= response.status_code < 600:\n                    print('  Server error on HTTP request', file=sys.stderr)\n                    continue\n\n                if response.status_code == 204:\n                    print('  No response', file=sys.stderr)\n                    continue\n\n                print('  Response:', response, file=sys.stderr)\n\n                try:\n                    data = json.loads(response.text)\n                except json.decoder.JSONDecodeError:\n                    print('! Json decode error: ', response.text, file=sys.stderr)\n                    continue\n\n                data['$schema'] = './raw.schema.json'\n\n                try:\n                    validator.validate(data)\n                except ValidationError as e:\n                    print('! Schema validation failed: ', e, file=sys.stderr)\n\n                    violations.append((date, campus))\n\n                with open(file, 'w') as f:\n                    json.dump(data, f, indent=2)\n\n    print('Violations:')\n    for violation in violations:\n        print(violation)\n"
  },
  {
    "path": "tests/external_menus/parsed.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"ParsedMenu\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": true,\n    \"date\": {\n      \"type\": \"string\",\n      \"format\": \"date\"\n    },\n    \"campus\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"cst\",\n        \"cde\",\n        \"cmi\",\n        \"cgb\",\n        \"cmu\",\n        \"hzs\"\n      ]\n    },\n    \"menu\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"external_id\": {\n            \"type\": \"integer\"\n          },\n          \"components\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"type\": \"object\",\n                  \"patternProperties\": {\n                    \"^[a-z][a-z]$\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"additionalProperties\": false\n                },\n                \"attributes\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"BIO\",\n                      \"CHICKEN\",\n                      \"GRILL\",\n                      \"CHEESE\",\n                      \"RABBIT\",\n                      \"LAMB\",\n                      \"PASTA\",\n                      \"VEAL\",\n                      \"SALAD\",\n                      \"SNACK\",\n                      \"SOUP\",\n                      \"PIG\",\n                      \"VEGAN\",\n                      \"VEGGIE\",\n                      \"FISH\",\n                      \"LESS_MEAT\"\n                    ]\n                  }\n                },\n                \"allergens\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"EGG\",\n                      \"WHEAT_GLUTEN\",\n                      \"LUPINE\",\n                      \"MILK_LACTOSE\",\n                      \"MUSTARD\",\n                      \"NUTS\",\n                      \"PEANUTS\",\n                      \"SHELLFISH\",\n                      \"CELERY\",\n                      \"SESAME\",\n                      \"SOY\",\n                      \"SULFITES\",\n                      \"FISH\",\n                      \"MOLLUSKS\",\n                      \"HALAL\"\n                    ]\n                  }\n                }\n              },\n              \"required\": [\n                \"name\",\n                \"attributes\",\n                \"allergens\"\n              ]\n            }\n          },\n          \"price\": {\n            \"type\": \"string\",\n            \"pattern\": \"([0-9]|[1-9][0-9]+)\\\\.[0-9][0-9]\"\n          },\n          \"multiple_prices\": {\n            \"type\": \"boolean\"\n          },\n          \"sort_order\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 11\n          }\n        },\n        \"required\": [\n          \"external_id\",\n          \"components\",\n          \"price\",\n          \"multiple_prices\"\n        ],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\n    \"date\",\n    \"campus\",\n    \"menu\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "tests/external_menus/processed.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"ProcessedMenu\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": true,\n    \"date\": {\n      \"type\": \"string\",\n      \"format\": \"date\"\n    },\n    \"campus\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"cst\",\n        \"cde\",\n        \"cmi\",\n        \"cgb\",\n        \"cmu\",\n        \"hzs\"\n      ]\n    },\n    \"menu\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"external_id\": {\n            \"type\": \"integer\"\n          },\n          \"name\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n              \"^[a-z][a-z]$\": {\n                \"type\": \"string\"\n              }\n            },\n            \"additionalProperties\": false\n          },\n          \"course_type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"SOUP\",\n              \"DAILY\",\n              \"PASTA\",\n              \"GRILL\",\n              \"SALAD\",\n              \"SUB\",\n              \"DESSERT\",\n              \"SNACK\"\n            ]\n          },\n          \"course_sub_type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"NORMAL\",\n              \"VEGETARIAN\",\n              \"VEGAN\"\n            ]\n          },\n          \"course_attributes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"BIO\",\n                \"CHICKEN\",\n                \"GRILL\",\n                \"CHEESE\",\n                \"RABBIT\",\n                \"LAMB\",\n                \"PASTA\",\n                \"VEAL\",\n                \"SALAD\",\n                \"SNACK\",\n                \"SOUP\",\n                \"PIG\",\n                \"VEGAN\",\n                \"VEGGIE\",\n                \"FISH\",\n                \"LESS_MEAT\"\n              ]\n            }\n          },\n          \"course_allergens\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"EGG\",\n                \"WHEAT_GLUTEN\",\n                \"LUPINE\",\n                \"MILK_LACTOSE\",\n                \"MUSTARD\",\n                \"NUTS\",\n                \"PEANUTS\",\n                \"SHELLFISH\",\n                \"CELERY\",\n                \"SESAME\",\n                \"SOY\",\n                \"SULFITES\",\n                \"FISH\",\n                \"MOLLUSKS\",\n                \"HALAL\"\n              ]\n            }\n          },\n          \"price_students\": {\n            \"type\": \"string\",\n            \"pattern\": \"([0-9]|[1-9][0-9]+)\\\\.[0-9][0-9]\"\n          },\n          \"price_staff\": {\n            \"oneOf\": [\n              {\n                \"type\": \"string\",\n                \"pattern\": \"([0-9]|[1-9][0-9]+)\\\\.[0-9][0-9]\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ]\n          }\n        },\n        \"required\": [\n          \"name\",\n          \"external_id\",\n          \"course_type\",\n          \"course_sub_type\",\n          \"course_attributes\",\n          \"course_allergens\",\n          \"price_students\",\n          \"price_staff\"\n        ],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\n    \"date\",\n    \"campus\",\n    \"menu\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "tests/external_menus/raw.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"definitions\": {\n    \"optional_string\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    }\n  },\n  \"title\": \"ExternalMenu\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": true,\n    \"id\": {\n      \"type\": \"integer\"\n    },\n    \"menuDate\": {\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"restaurantId\": {\n      \"type\": \"integer\"\n    },\n    \"chefId\": {\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 0\n    },\n    \"description\": {\n      \"type\": \"null\"\n    },\n    \"approvedById\": {\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 0\n    },\n    \"approvedDateTime\": {\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"approved\": {\n      \"type\": \"boolean\",\n      \"enum\": [\n        false\n      ]\n    },\n    \"requestToBeApproved\": {\n      \"type\": \"boolean\",\n      \"enum\": [\n        false\n      ]\n    },\n    \"remark\": {\n      \"type\": \"null\"\n    },\n    \"menuItems\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"integer\"\n          },\n          \"nameNl\": {\n            \"type\": \"null\"\n          },\n          \"nameEn\": {\n            \"type\": \"null\"\n          },\n          \"menuTypeId\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 0\n          },\n          \"chefId\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 0\n          },\n          \"enabled\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 2\n          },\n          \"remark\": {\n            \"type\": \"null\"\n          },\n          \"menuid\": {\n            \"type\": \"integer\"\n          },\n          \"sortorder\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 11\n          },\n          \"menuItemContents\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"id\": {\n                  \"type\": \"integer\"\n                },\n                \"menuItemId\": {\n                  \"type\": \"integer\"\n                },\n                \"courseId\": {\n                  \"type\": \"integer\"\n                },\n                \"sortOrder\": {\n                  \"type\": \"integer\",\n                  \"minimum\": 0,\n                  \"maximum\": 0\n                },\n                \"course\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\"\n                    },\n                    \"dispNameNl\": {\n                      \"type\": \"string\"\n                    },\n                    \"dispNameEn\": {\n                      \"type\": \"string\"\n                    },\n                    \"nameNl\": {\n                      \"type\": \"string\"\n                    },\n                    \"nameEn\": {\n                      \"type\": \"string\"\n                    },\n                    \"weight\": {\n                      \"$ref\": \"#/definitions/optional_string\"\n                    },\n                    \"extra\": {\n                      \"$ref\": \"#/definitions/optional_string\"\n                    },\n                    \"preparation\": {\n                      \"$ref\": \"#/definitions/optional_string\"\n                    },\n                    \"price\": {\n                      \"type\": \"number\",\n                      \"minimum\": 0.0\n                    },\n                    \"photo\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"\"\n                      ]\n                    },\n                    \"isCourse\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"isIngredient\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"course_CategoryForCourses\": {\n                      \"type\": \"null\"\n                    },\n                    \"course_Allergens\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"courseId\": {\n                            \"type\": \"integer\"\n                          },\n                          \"allergenId\": {\n                            \"type\": \"integer\"\n                          }\n                        },\n                        \"required\": [\n                          \"courseId\",\n                          \"allergenId\"\n                        ],\n                        \"additionalProperties\": false\n                      }\n                    },\n                    \"course_CourseLogos\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"courseId\": {\n                            \"type\": \"integer\"\n                          },\n                          \"courseLogoId\": {\n                            \"type\": \"integer\"\n                          }\n                        },\n                        \"required\": [\n                          \"courseId\",\n                          \"courseLogoId\"\n                        ],\n                        \"additionalProperties\": false\n                      }\n                    },\n                    \"maincourse\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"menuInfo\": {\n                      \"$ref\": \"#/definitions/optional_string\"\n                    },\n                    \"fixedMultiplePrices\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"calculatedMultiplePrices\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"fixedprice\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"showFirst\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"deleted\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"enabled\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"menuInfoEn\": {\n                      \"$ref\": \"#/definitions/optional_string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"dispNameNl\",\n                    \"dispNameEn\",\n                    \"nameNl\",\n                    \"nameEn\",\n                    \"weight\",\n                    \"extra\",\n                    \"preparation\",\n                    \"price\",\n                    \"photo\",\n                    \"isCourse\",\n                    \"isIngredient\",\n                    \"course_CategoryForCourses\",\n                    \"course_Allergens\",\n                    \"course_CourseLogos\",\n                    \"maincourse\",\n                    \"menuInfo\",\n                    \"fixedMultiplePrices\",\n                    \"calculatedMultiplePrices\",\n                    \"fixedprice\",\n                    \"showFirst\",\n                    \"deleted\",\n                    \"enabled\",\n                    \"menuInfoEn\"\n                  ],\n                  \"additionalProperties\": false\n                }\n              },\n              \"required\": [\n                \"id\",\n                \"menuItemId\",\n                \"courseId\",\n                \"sortOrder\",\n                \"course\"\n              ]\n            }\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"nameNl\",\n          \"nameEn\",\n          \"menuTypeId\",\n          \"chefId\",\n          \"enabled\",\n          \"remark\",\n          \"menuid\",\n          \"sortorder\",\n          \"menuItemContents\"\n        ],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\n    \"id\",\n    \"menuDate\",\n    \"restaurantId\",\n    \"chefId\",\n    \"description\",\n    \"approvedById\",\n    \"approvedDateTime\",\n    \"approved\",\n    \"requestToBeApproved\",\n    \"remark\",\n    \"menuItems\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "tests/test_debug_state.py",
    "content": "import unittest\n\nfrom komidabot.debug.state import DebuggableException, ProgramStateTrace, SimpleProgramState\n\n\nclass TestConstants(unittest.TestCase):\n    \"\"\"\n    Tests to see if komidabot.debug.state works properly.\n    \"\"\"\n\n    def test_no_raise(self):\n        # Checks that ProgramStateTrace.state does not raise on its self.\n        debug_state = ProgramStateTrace()\n\n        with debug_state.state(SimpleProgramState('Test state')):\n            pass\n\n    def test_simple_raise(self):\n        # Checks that ProgramStateTrace.state catches exceptions and rethrows them as DebuggableException.\n        debug_state = ProgramStateTrace()\n\n        with self.assertRaises(DebuggableException) as caught:\n            with debug_state.state(SimpleProgramState('Test state 1')):\n                raise Exception('Test exception')\n\n        expected = '\\n'.join([\n            \"Program state trace:\",\n            \"- InitialState\",\n            \"- State('Test state 1', None)\",\n        ])\n        ex: DebuggableException = caught.exception\n\n        self.assertEqual(expected, repr(ex.get_trace()))\n\n    def test_simple_nested(self):\n        # Checks that simple nested states are properly returned\n        debug_state = ProgramStateTrace()\n\n        with self.assertRaises(DebuggableException) as caught:\n            with debug_state.state(SimpleProgramState('Test state 1')):\n                with debug_state.state(SimpleProgramState('Test state 2')):\n                    raise Exception('Test exception')\n\n        expected = '\\n'.join([\n            \"Program state trace:\",\n            \"- InitialState\",\n            \"- State('Test state 1', None)\",\n            \"- State('Test state 2', None)\",\n        ])\n        ex: DebuggableException = caught.exception\n\n        self.assertEqual(expected, repr(ex.get_trace()))\n\n    def test_simple_branched(self):\n        # Checks that only the branch where the exception occurred is returned\n        debug_state = ProgramStateTrace()\n\n        with self.assertRaises(DebuggableException) as caught:\n            with debug_state.state(SimpleProgramState('Test state 1')):\n                with debug_state.state(SimpleProgramState('Test state 1a')):\n                    pass\n                with debug_state.state(SimpleProgramState('Test state 1b')):\n                    pass\n                with debug_state.state(SimpleProgramState('Test state 1c')):\n                    pass\n            with debug_state.state(SimpleProgramState('Test state 2')):\n                with debug_state.state(SimpleProgramState('Test state 2a')):\n                    pass\n                with debug_state.state(SimpleProgramState('Test state 2b')):\n                    raise Exception('Test exception')\n                with debug_state.state(SimpleProgramState('Test state 2c')):\n                    pass\n            with debug_state.state(SimpleProgramState('Test state 3')):\n                with debug_state.state(SimpleProgramState('Test state 3a')):\n                    pass\n                with debug_state.state(SimpleProgramState('Test state 3b')):\n                    pass\n                with debug_state.state(SimpleProgramState('Test state 3c')):\n                    pass\n\n        expected = '\\n'.join([\n            \"Program state trace:\",\n            \"- InitialState\",\n            \"- State('Test state 2', None)\",\n            \"- State('Test state 2b', None)\",\n        ])\n        ex: DebuggableException = caught.exception\n\n        self.assertEqual(expected, repr(ex.get_trace()))\n\n    def test_multi_nested(self):\n        # Checks that nested states from different traces are properly returned\n        debug_state1 = ProgramStateTrace()\n\n        with self.assertRaises(DebuggableException) as caught:\n            with debug_state1.state(SimpleProgramState('Test state 1')):\n                debug_state2 = ProgramStateTrace()\n\n                with debug_state2.state(SimpleProgramState('Test state 2')):\n                    raise Exception('Test exception')\n\n        expected = '\\n'.join([\n            \"Program state trace:\",\n            \"- InitialState\",\n            \"- State('Test state 1', None)\",\n            \"- InitialState\",\n            \"- State('Test state 2', None)\",\n        ])\n        ex: DebuggableException = caught.exception\n\n        self.assertEqual(expected, repr(ex.get_trace()))\n\n    def test_multi_branched(self):\n        # Checks that only the branch where the exception occurred is returned, even if we have different traces\n        debug_state1 = ProgramStateTrace()\n\n        with self.assertRaises(DebuggableException) as caught:\n            with debug_state1.state(SimpleProgramState('Test state 1')):\n                debug_state2 = ProgramStateTrace()\n                with debug_state2.state(SimpleProgramState('Test state 1a')):\n                    pass\n                with debug_state2.state(SimpleProgramState('Test state 1b')):\n                    pass\n                with debug_state2.state(SimpleProgramState('Test state 1c')):\n                    pass\n            with debug_state1.state(SimpleProgramState('Test state 2')):\n                debug_state2 = ProgramStateTrace()\n                with debug_state2.state(SimpleProgramState('Test state 2a')):\n                    pass\n                with debug_state2.state(SimpleProgramState('Test state 2b')):\n                    raise Exception('Test exception')\n                with debug_state2.state(SimpleProgramState('Test state 2c')):\n                    pass\n            with debug_state1.state(SimpleProgramState('Test state 3')):\n                debug_state2 = ProgramStateTrace()\n                with debug_state2.state(SimpleProgramState('Test state 3a')):\n                    pass\n                with debug_state2.state(SimpleProgramState('Test state 3b')):\n                    pass\n                with debug_state2.state(SimpleProgramState('Test state 3c')):\n                    pass\n\n        expected = '\\n'.join([\n            \"Program state trace:\",\n            \"- InitialState\",\n            \"- State('Test state 2', None)\",\n            \"- InitialState\",\n            \"- State('Test state 2b', None)\",\n        ])\n        ex: DebuggableException = caught.exception\n\n        self.assertEqual(expected, repr(ex.get_trace()))\n"
  },
  {
    "path": "tests/test_external_menu.py",
    "content": "import glob\nimport json\nimport os\nimport re\nfrom typing import Any, Dict, List, Union\n\nimport yaml\nfrom jsonschema import Draft7Validator\n\nimport komidabot.external_menu as external_menu\nimport komidabot.models as models\nfrom extensions import db\nfrom tests.base import BaseTestCase, HttpCapture\n\n\ndef filter_meta(value: Union[List[Any], Dict[str, Any]]):\n    if isinstance(value, dict):\n        for key in list(value.keys()):\n            if key.startswith('$'):\n                del value[key]\n            else:\n                filter_meta(value[key])\n    elif isinstance(value, list):\n        for item in value:\n            filter_meta(item)\n\n\nclass TestExternalMenu(BaseTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.campuses = {\n            'cst': models.Campus.create('Stadscampus', 'cst', ['stad', 'stadscampus'], 1),\n            'cde': models.Campus.create('Campus Drie Eiken', 'cde', ['drie', 'eiken'], 2),\n            'cmi': models.Campus.create('Campus Middelheim', 'cmi', ['middelheim'], 3),\n            'cgb': models.Campus.create('Campus Groenenborger', 'cgb', ['groenenborger'], 4),\n            'cmu': models.Campus.create('Campus Mutsaard', 'cmu', ['mutsaard'], 5),\n            'hzs': models.Campus.create('Hogere Zeevaartschool', 'hzs', ['hogere', 'zeevaartschool'], 6),\n        }\n\n        db.session.commit()\n\n        self.assertEqual(self.campuses['cst'].id, 1)\n\n        self.validator_raw = TestExternalMenu.create_validator('raw.schema.json')\n        self.validator_parsed = TestExternalMenu.create_validator('parsed.schema.json')\n        self.validator_processed = TestExternalMenu.create_validator('processed.schema.json')\n\n    @staticmethod\n    def create_validator(schema):\n        with open(os.path.join(os.path.dirname(__file__), 'external_menus', schema)) as f:\n            schema = json.load(f)\n\n        Draft7Validator.check_schema(schema)\n        return Draft7Validator(schema)\n\n    def test_saved_requests(self):\n        saved_files = glob.glob(os.path.join(os.path.dirname(__file__), 'external_menus', '*.raw.json'))\n\n        self.maxDiff = 5000\n\n        old_convert_price = external_menu._convert_price\n\n        # conversion_table = {}\n\n        def _convert_price(price_students):\n            price_students = str(price_students)\n\n            # nonlocal conversion_table\n            #\n            # if price_students in conversion_table:\n            #     return conversion_table[price_students]\n            #\n            # conversion_table[price_students] = old_convert_price(price_students)\n            #\n            # return conversion_table[price_students]\n\n            return {\n                '3.20': '4.00',  # external ID 1\n                '3.40': '4.20',  # external ID 2\n                '3.60': '4.50',  # external ID 3\n                '3.80': '4.70',  # external ID 4\n                '4.00': '5.00',  # external ID 5\n                '4.20': '5.20',  # external ID 6\n                '4.40': '5.50',  # external ID 7\n                '4.60': '5.70',  # external ID 8\n                '4.80': '6.00',  # external ID 9\n                '5.00': '6.20',  # external ID 10\n                '5.20': '6.50',  # external ID 11\n                '5.40': '6.70',  # external ID 12\n                '5.60': '7.00',  # external ID 13\n            }.get(price_students, price_students)\n\n        external_menu._convert_price = _convert_price\n\n        for saved_file in sorted(saved_files):\n            with HttpCapture():  # Ensure no requests are made\n                with self.subTest(file=os.path.basename(saved_file)):\n                    with self.app.app_context():\n                        parsed_out = re.sub(r'raw\\.json$', 'parsed.yaml', saved_file)\n                        parsed_expected = re.sub(r'raw\\.json$', 'parsed.expected.yaml', saved_file)\n                        processed_out = re.sub(r'raw\\.json$', 'processed.yaml', saved_file)\n                        processed_expected = re.sub(r'raw\\.json$', 'processed.expected.yaml', saved_file)\n\n                        with open(saved_file, 'r') as f:\n                            data_raw = json.load(f)\n\n                        self.validator_raw.validate(data_raw)\n\n                        data_parsed = external_menu.parse_fetched(data_raw)\n\n                        with open(parsed_out, 'w') as f:\n                            yaml.safe_dump(data_parsed, f, indent=2)\n\n                        if os.path.exists(parsed_expected):\n                            with open(parsed_expected, 'r') as f:\n                                data_parsed_expected = yaml.safe_load(f)\n\n                            filter_meta(data_parsed_expected)\n\n                            self.assertEqual(yaml.safe_dump(data_parsed_expected), yaml.safe_dump(data_parsed))\n\n                            # If we already know what is expected, this file will contain the same contents and as such\n                            # we do not need to keep it around\n                            os.remove(parsed_out)\n\n                        self.validator_parsed.validate(data_parsed)\n\n                        data_processed = external_menu.process_parsed(data_parsed)\n\n                        with open(processed_out, 'w') as f:\n                            yaml.safe_dump(data_processed, f, indent=2)\n\n                        if os.path.exists(processed_expected):\n                            with open(processed_expected, 'r') as f:\n                                data_processed_expected = yaml.safe_load(f)\n\n                            filter_meta(data_processed_expected)\n\n                            self.assertEqual(yaml.safe_dump(data_processed_expected), yaml.safe_dump(data_processed))\n\n                            # If we already know what is expected, this file will contain the same contents and as such\n                            # we do not need to keep it around\n                            os.remove(processed_out)\n\n                        self.validator_processed.validate(data_processed)\n\n                        # Try and update the menu, this shouldn't cause any issues really.\n                        # However we won't check if this was added properly to the database,\n                        # different tests should cover this\n                        external_menu.update_menu(data_processed)\n\n        external_menu._convert_price = old_convert_price\n"
  },
  {
    "path": "tests/test_models_campus.py",
    "content": "from sqlalchemy import inspect\n\nimport komidabot.models as models\nfrom app import db\nfrom tests.base import BaseTestCase\n\n\nclass TestModelsCampus(BaseTestCase):\n    \"\"\"\n    Test models.Campus\n    \"\"\"\n\n    def test_simple_constructors(self):\n        # Test constructor of Campus model\n\n        with self.app.app_context():\n            campus1 = models.Campus('Testcampus', 'ctst')\n            campus1.external_id = 900\n            campus2 = models.Campus('Campus Omega', 'com')\n            campus2.external_id = 800\n            campus3 = models.Campus('Campus Paardenmarkt', 'cpm')\n            campus3.external_id = 700\n\n            # Ensure that the constructor does not add the entities to the database\n            self.assertTrue(inspect(campus1).transient)\n            self.assertTrue(inspect(campus2).transient)\n            self.assertTrue(inspect(campus3).transient)\n\n            db.session.add(campus1)\n            db.session.add(campus2)\n            db.session.add(campus3)\n\n            # Flush makes sure default values are actually assigned\n            db.session.flush()\n\n            self.assertTrue(campus1.active)\n            self.assertTrue(campus2.active)\n            self.assertTrue(campus3.active)\n\n            db.session.commit()\n\n    # noinspection PyTypeChecker\n    def test_invalid_constructors(self):\n        # Test constructor of Campus model\n\n        with self.app.app_context():\n            with self.assertRaises(ValueError):\n                models.Campus(None, 'ctst')\n\n            with self.assertRaises(ValueError):\n                models.Campus(13, 'ctst')\n\n            with self.assertRaises(ValueError):\n                models.Campus('Testcampus', None)\n\n            with self.assertRaises(ValueError):\n                models.Campus('Testcampus', 42)\n\n    def test_create(self):\n        # Test usage of Campus.create with add_to_db set to True\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900,\n                                           add_to_db=True)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800,\n                                           add_to_db=True)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700,\n                                           add_to_db=True)\n\n            # Ensure that the create method adds the entities to the database\n            self.assertFalse(inspect(campus1).transient)\n            self.assertFalse(inspect(campus2).transient)\n            self.assertFalse(inspect(campus3).transient)\n\n            # Flush makes sure default values are actually assigned\n            db.session.flush()\n\n            self.assertTrue(campus1.active)\n            self.assertTrue(campus2.active)\n            self.assertTrue(campus3.active)\n\n            db.session.commit()\n\n    def test_create_no_add_to_db(self):\n        # Test usage of Campus.create with add_to_db set to False\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900,\n                                           add_to_db=False)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800,\n                                           add_to_db=False)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700,\n                                           add_to_db=False)\n\n            # Ensure that the create method does not add the entities to the database\n            self.assertTrue(inspect(campus1).transient)\n            self.assertTrue(inspect(campus2).transient)\n            self.assertTrue(inspect(campus3).transient)\n\n            db.session.add(campus1)\n            db.session.add(campus2)\n            db.session.add(campus3)\n\n            db.session.commit()\n\n    # FIXME: Duplicate keywords will not be allowed in the near future\n    def test_keywords(self):\n        # Test keywords methods\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n\n            db.session.commit()\n\n            kw1 = campus1.get_keywords()\n            kw2 = campus2.get_keywords()\n            kw3 = campus3.get_keywords()\n\n            # Campus 1\n            self.assertIn('ctst', kw1)\n            self.assertNotIn('com', kw1)\n            self.assertNotIn('cpm', kw1)\n            self.assertIn('keyword1', kw1)\n            self.assertNotIn('keyword2', kw1)\n            self.assertNotIn('keyword3', kw1)\n            self.assertIn('shared_keyword', kw1)\n            self.assertNotIn('extra_keyword', kw1)\n            # Campus 2\n            self.assertNotIn('ctst', kw2)\n            self.assertIn('com', kw2)\n            self.assertNotIn('cpm', kw2)\n            self.assertNotIn('keyword1', kw2)\n            self.assertIn('keyword2', kw2)\n            self.assertNotIn('keyword3', kw2)\n            self.assertIn('shared_keyword', kw2)\n            self.assertNotIn('extra_keyword', kw2)\n            # Campus 3\n            self.assertNotIn('ctst', kw3)\n            self.assertNotIn('com', kw3)\n            self.assertIn('cpm', kw3)\n            self.assertNotIn('keyword1', kw3)\n            self.assertNotIn('keyword2', kw3)\n            self.assertIn('keyword3', kw3)\n            self.assertIn('shared_keyword', kw3)\n            self.assertNotIn('extra_keyword', kw3)\n\n            campus1.remove_keyword('keyword1')\n            campus3.add_keyword('extra_keyword')\n\n            db.session.commit()\n\n            kw1 = campus1.get_keywords()\n            kw2 = campus2.get_keywords()\n            kw3 = campus3.get_keywords()\n\n            # Campus 1\n            self.assertIn('ctst', kw1)\n            self.assertNotIn('com', kw1)\n            self.assertNotIn('cpm', kw1)\n            self.assertNotIn('keyword1', kw1)\n            self.assertNotIn('keyword2', kw1)\n            self.assertNotIn('keyword3', kw1)\n            self.assertIn('shared_keyword', kw1)\n            self.assertNotIn('extra_keyword', kw1)\n            # Campus 2\n            self.assertNotIn('ctst', kw2)\n            self.assertIn('com', kw2)\n            self.assertNotIn('cpm', kw2)\n            self.assertNotIn('keyword1', kw2)\n            self.assertIn('keyword2', kw2)\n            self.assertNotIn('keyword3', kw2)\n            self.assertIn('shared_keyword', kw2)\n            self.assertNotIn('extra_keyword', kw2)\n            # Campus 3\n            self.assertNotIn('ctst', kw3)\n            self.assertNotIn('com', kw3)\n            self.assertIn('cpm', kw3)\n            self.assertNotIn('keyword1', kw3)\n            self.assertNotIn('keyword2', kw3)\n            self.assertIn('keyword3', kw3)\n            self.assertIn('shared_keyword', kw3)\n            self.assertIn('extra_keyword', kw3)\n\n            with self.assertRaises(ValueError):\n                campus1.add_keyword('keyword with spaces')\n\n    def test_get_by_id(self):\n        # Test getting a Campus object by its ID\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n\n            db.session.commit()\n\n            self.assertEqual(campus1, models.Campus.get_by_id(campus1.id))\n            self.assertEqual(campus2, models.Campus.get_by_id(campus2.id))\n            self.assertEqual(campus3, models.Campus.get_by_id(campus3.id))\n\n    def test_get_by_external_id(self):\n        # Test getting a Campus object by its ID\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n\n            db.session.commit()\n\n            self.assertEqual(campus1, models.Campus.get_by_external_id(campus1.external_id))\n            self.assertEqual(campus2, models.Campus.get_by_external_id(campus2.external_id))\n            self.assertEqual(campus3, models.Campus.get_by_external_id(campus3.external_id))\n\n    def test_get_by_short_name(self):\n        # Test getting a Campus object by its short name\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n\n            db.session.commit()\n\n            self.assertEqual(campus1, models.Campus.get_by_short_name('ctst'))\n            self.assertEqual(campus2, models.Campus.get_by_short_name('com'))\n            self.assertEqual(campus3, models.Campus.get_by_short_name('cpm'))\n\n    # FIXME: Duplicate keywords will not be allowed in the near future -> Results will be Optional[Campus]\n    def test_find_by_keyword(self):\n        # Test getting campuses by a keyword\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n            campus3.active = False\n\n            db.session.commit()\n\n            self.assertEqual(models.Campus.find_by_keyword('ctst'), [campus1])\n            self.assertEqual(models.Campus.find_by_keyword('com'), [campus2])\n            self.assertEqual(models.Campus.find_by_keyword('cpm'), [campus3])\n            self.assertEqual(models.Campus.find_by_keyword('keyword1'), [campus1])\n            self.assertEqual(models.Campus.find_by_keyword('keyword2'), [campus2])\n            self.assertEqual(models.Campus.find_by_keyword('keyword3'), [campus3])\n\n            campuses = models.Campus.find_by_keyword('shared_keyword')\n            ids = [campus.id for campus in campuses]\n\n            self.assertEqual(len(campuses), 3)\n            self.assertEqual(len(ids), 3)\n            self.assertIn(campus1.id, ids)\n            self.assertIn(campus2.id, ids)\n            self.assertIn(campus3.id, ids)\n\n    def test_get_all(self):\n        # Test getting all campuses\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n            campus3.active = False\n\n            db.session.commit()\n\n            campuses = models.Campus.get_all()\n            ids = [campus.id for campus in campuses]\n\n            self.assertEqual(len(campuses), 3)\n            self.assertEqual(len(ids), 3)\n            self.assertIn(campus1.id, ids)\n            self.assertIn(campus2.id, ids)\n            self.assertIn(campus3.id, ids)\n\n    def test_get_all_active(self):\n        # Test getting all campuses marked as active\n\n        with self.app.app_context():\n            campus1 = models.Campus.create('Testcampus', 'ctst', ['keyword1', 'shared_keyword'], 900)\n            campus2 = models.Campus.create('Campus Omega', 'com', ['keyword2', 'shared_keyword'], 800)\n            campus3 = models.Campus.create('Campus Paardenmarkt', 'cpm', ['keyword3', 'shared_keyword'], 700)\n            campus3.active = False\n\n            db.session.commit()\n\n            campuses = models.Campus.get_all_active()\n            ids = [campus.id for campus in campuses]\n\n            self.assertEqual(len(campuses), 2)\n            self.assertEqual(len(ids), 2)\n            self.assertIn(campus1.id, ids)\n            self.assertIn(campus2.id, ids)\n            self.assertNotIn(campus3.id, ids)\n"
  },
  {
    "path": "tests/test_models_closing_days.py",
    "content": "from sqlalchemy import inspect\n\nimport komidabot.models as models\nimport tests.utils as utils\nfrom app import db\nfrom tests.base import BaseTestCase\n\n\nclass TestModelsClosingDays(BaseTestCase):\n    \"\"\"\n    Test models.ClosingDays\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.create_test_campuses()\n\n    def test_simple_constructors(self):\n        # Test constructor of ClosingDays model\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en'}, 'en', has_context=True)\n            translatable2, _ = self.create_translation({'en': 'Translation 2: en'}, 'en', has_context=True)\n            translatable3, _ = self.create_translation({'en': 'Translation 3: en'}, 'en', has_context=True)\n\n            closed1 = models.ClosingDays(self.campuses[0].id, utils.DAYS['MON'], utils.DAYS['MON'], translatable1.id)\n            closed2 = models.ClosingDays(self.campuses[1].id, utils.DAYS['TUE'], utils.DAYS['FRI'], translatable2.id)\n            closed3 = models.ClosingDays(self.campuses[2].id, utils.DAYS['THU'], utils.DAYS['THU'], translatable3.id)\n\n            # Ensure that the constructor does not add the entities to the database\n            self.assertTrue(inspect(closed1).transient)\n            self.assertTrue(inspect(closed2).transient)\n            self.assertTrue(inspect(closed3).transient)\n\n            db.session.add(closed1)\n            db.session.add(closed2)\n            db.session.add(closed3)\n\n            db.session.commit()\n\n    # noinspection PyTypeChecker\n    def test_invalid_constructors(self):\n        # Test constructor of Campus model\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en'}, 'en', has_context=True)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(None, utils.DAYS['MON'], utils.DAYS['MON'], translatable1.id)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays('id', utils.DAYS['MON'], utils.DAYS['MON'], translatable1.id)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(self.campuses[0].id, None, utils.DAYS['MON'], translatable1.id)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(self.campuses[0].id, '2002-02-20', utils.DAYS['MON'], translatable1.id)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(self.campuses[0].id, utils.DAYS['MON'], '2002-02-20', translatable1.id)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(self.campuses[0].id, utils.DAYS['MON'], utils.DAYS['MON'], None)\n\n            with self.assertRaises(ValueError):\n                models.ClosingDays(self.campuses[0].id, utils.DAYS['MON'], utils.DAYS['MON'], 'translatable')\n\n    def test_create(self):\n        # Test usage of ClosingDays.create with add_to_db set to True\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            closed1 = models.ClosingDays.create(self.campuses[0], utils.DAYS['MON'], utils.DAYS['MON'],\n                                                'Translation 1: en', 'en', add_to_db=True)\n            closed2 = models.ClosingDays.create(self.campuses[1], utils.DAYS['TUE'], utils.DAYS['FRI'],\n                                                'Translation 2: en', 'en', add_to_db=True)\n            closed3 = models.ClosingDays.create(self.campuses[2], utils.DAYS['THU'], utils.DAYS['THU'],\n                                                'Translation 3: en', 'en', add_to_db=True)\n\n            # Ensure that the create method adds the entities to the database\n            self.assertFalse(inspect(closed1).transient)\n            self.assertFalse(inspect(closed2).transient)\n            self.assertFalse(inspect(closed3).transient)\n\n            db.session.commit()\n\n    def test_create_no_add_to_db(self):\n        # Test usage of Campus.create with add_to_db set to False\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            closed1 = models.ClosingDays.create(self.campuses[0], utils.DAYS['MON'], utils.DAYS['MON'],\n                                                'Translation 1: en', 'en', add_to_db=False)\n            closed2 = models.ClosingDays.create(self.campuses[1], utils.DAYS['TUE'], utils.DAYS['FRI'],\n                                                'Translation 2: en', 'en', add_to_db=False)\n            closed3 = models.ClosingDays.create(self.campuses[2], utils.DAYS['THU'], utils.DAYS['THU'],\n                                                'Translation 3: en', 'en', add_to_db=False)\n\n            # Ensure that the create method does not add the entities to the database\n            self.assertTrue(inspect(closed1).transient)\n            self.assertTrue(inspect(closed2).transient)\n            self.assertTrue(inspect(closed3).transient)\n\n            db.session.add(closed1)\n            db.session.add(closed2)\n            db.session.add(closed3)\n\n            db.session.commit()\n\n    def test_find_is_closed(self):\n        # Test finding if a campus is closed on a specific day\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            closed1 = models.ClosingDays.create(self.campuses[0], utils.DAYS['TUE'], utils.DAYS['TUE'],\n                                                'Translation 1: en', 'en')\n            closed2 = models.ClosingDays.create(self.campuses[1], utils.DAYS['TUE'], utils.DAYS['THU'],\n                                                'Translation 2: en', 'en')\n            closed3 = models.ClosingDays.create(self.campuses[2], utils.DAYS['WED'], None,\n                                                'Translation 3: en', 'en')\n\n            db.session.commit()\n\n            # Campus 1\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[0], utils.DAYS['MON']))\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[0], utils.DAYS['TUE']), closed1)\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[0], utils.DAYS['WED']))\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[0], utils.DAYS['THU']))\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[0], utils.DAYS['FRI']))\n            # Campus 2\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[1], utils.DAYS['MON']))\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[1], utils.DAYS['TUE']), closed2)\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[1], utils.DAYS['WED']), closed2)\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[1], utils.DAYS['THU']), closed2)\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[1], utils.DAYS['FRI']))\n            # Campus 3\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[2], utils.DAYS['MON']))\n            self.assertIsNone(models.ClosingDays.find_is_closed(self.campuses[2], utils.DAYS['TUE']))\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[2], utils.DAYS['WED']), closed3)\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[2], utils.DAYS['THU']), closed3)\n            self.assertEqual(models.ClosingDays.find_is_closed(self.campuses[2], utils.DAYS['FRI']), closed3)\n\n    def test_find_closing_days_including(self):\n        pass  # TODO\n"
  },
  {
    "path": "tests/test_models_menu.py",
    "content": "from decimal import Decimal\n\nimport tests.utils as utils\nfrom app import db\nfrom komidabot.models import Menu, MenuItem, CourseType, CourseSubType, CourseAttributes\nfrom tests.base import BaseTestCase\n\n\nclass TestModelsMenu(BaseTestCase):\n    \"\"\"\n    Test models.Menu\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.create_test_campuses()\n\n    def test_simple_constructors(self):\n        # Test constructor of Menu model\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            # XXX: Use constructor here to test, rather than the appropriate method\n            menu1 = Menu(self.campuses[0].id, utils.DAYS['MON'])\n            menu2 = Menu(self.campuses[1].id, utils.DAYS['TUE'])\n            menu3 = Menu(self.campuses[0].id, utils.DAYS['WED'])\n            menu4 = Menu(self.campuses[1].id, utils.DAYS['THU'])\n            menu5 = Menu(self.campuses[0].id, utils.DAYS['FRI'])\n\n            db.session.add(menu1)\n            db.session.add(menu2)\n            db.session.add(menu3)\n            db.session.add(menu4)\n            db.session.add(menu5)\n            db.session.commit()\n\n    # noinspection PyTypeChecker\n    def test_invalid_constructors(self):\n        # Test constructor of Campus model\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            with self.assertRaises(ValueError):\n                Menu(None, utils.DAYS['MON'])\n\n            with self.assertRaises(ValueError):\n                Menu('id', utils.DAYS['MON'])\n\n            with self.assertRaises(ValueError):\n                Menu(self.campuses[0].id, None)\n\n            with self.assertRaises(ValueError):\n                Menu(self.campuses[0].id, '2020-02-20')\n\n    def test_create(self):\n        # Test usage of Menu.create to check if Menus are constructed the same way as through their constructor\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            Menu.create(self.campuses[0], utils.DAYS['MON'])\n            Menu.create(self.campuses[1], utils.DAYS['TUE'])\n            Menu.create(self.campuses[0], utils.DAYS['WED'])\n            Menu.create(self.campuses[1], utils.DAYS['THU'])\n            Menu.create(self.campuses[0], utils.DAYS['FRI'])\n\n            db.session.commit()\n\n    def test_create_no_add_first(self):\n        # Tests usage of Menu.create with add_to_db=False, and manually adding it afterwards\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en'}, 'en', has_context=True)\n            translatable2, _ = self.create_translation({'en': 'Translation 2: en'}, 'en', has_context=True)\n            translatable3, _ = self.create_translation({'en': 'Translation 3: en'}, 'en', has_context=True)\n\n            menu = Menu.create(self.campuses[0], utils.DAYS['MON'], add_to_db=False)\n\n            menu_item1 = menu.add_menu_item(translatable1, CourseType.SUB, CourseSubType.NORMAL,\n                                            [CourseAttributes.SNACK], [], Decimal('1.0'), None)\n            menu_item2 = menu.add_menu_item(translatable2, CourseType.PASTA, CourseSubType.NORMAL,\n                                            [CourseAttributes.PASTA], [], Decimal('1.0'), Decimal('4.0'))\n            menu_item3 = menu.add_menu_item(translatable3, CourseType.SOUP, CourseSubType.VEGAN,\n                                            [CourseAttributes.SOUP], [], Decimal('1.0'), Decimal('2.0'))\n\n            db.session.add(menu)\n            db.session.commit()\n\n            items = MenuItem.query.filter_by(menu_id=menu.id).order_by(MenuItem.id).all()\n\n            self.assertIn(menu_item1, items)\n            self.assertIn(menu_item2, items)\n            self.assertIn(menu_item3, items)\n"
  },
  {
    "path": "tests/test_models_menu_item.py",
    "content": "from decimal import Decimal\n\nimport tests.utils as utils\nfrom app import db\nfrom komidabot.models import Menu, MenuItem, CourseType, CourseSubType, CourseAttributes\nfrom tests.base import BaseTestCase\n\n\nclass TestModelsMenuItem(BaseTestCase):\n    \"\"\"\n    Test models.MenuItem\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.create_test_campuses()\n\n    def test_simple_constructors(self):\n        # Test constructor of MenuItem model\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en'}, 'en', has_context=True)\n            translatable2, _ = self.create_translation({'en': 'Translation 2: en'}, 'en', has_context=True)\n            translatable3, _ = self.create_translation({'en': 'Translation 3: en'}, 'en', has_context=True)\n\n            menu = Menu.create(self.campuses[0], utils.DAYS['MON'])\n\n            # Required if we need to get menu.id, otherwise it would return None\n            # db.session.flush()\n\n            # XXX: Use constructor here to test, rather than the appropriate method\n            MenuItem(menu, translatable1.id, CourseType.SUB, CourseSubType.NORMAL, Decimal('1.0'), None)\n            MenuItem(menu, translatable2.id, CourseType.PASTA, CourseSubType.NORMAL, Decimal('1.0'), Decimal('4.0'))\n            MenuItem(menu, translatable3.id, CourseType.SOUP, CourseSubType.VEGAN, Decimal('1.0'), Decimal('2.0'))\n\n            db.session.commit()\n\n    def test_add_menu_item(self):\n        # Test usage of Menu.add_menu_item to check if MenuItems are constructed the same way as through their\n        # constructor\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en'}, 'en', has_context=True)\n            translatable2, _ = self.create_translation({'en': 'Translation 2: en'}, 'en', has_context=True)\n            translatable3, _ = self.create_translation({'en': 'Translation 3: en'}, 'en', has_context=True)\n\n            menu = Menu.create(self.campuses[0], utils.DAYS['MON'])\n\n            # Required if we need to get menu.id, otherwise it would return None\n            # db.session.flush()\n\n            menu_item1 = menu.add_menu_item(translatable1, CourseType.SUB, CourseSubType.NORMAL,\n                                            [CourseAttributes.SNACK], [], Decimal('1.0'), None)\n            menu_item2 = menu.add_menu_item(translatable2, CourseType.PASTA, CourseSubType.NORMAL,\n                                            [CourseAttributes.PASTA], [], Decimal('1.0'), Decimal('4.0'))\n            menu_item3 = menu.add_menu_item(translatable3, CourseType.SOUP, CourseSubType.VEGAN,\n                                            [CourseAttributes.SOUP], [], Decimal('1.0'), Decimal('2.0'))\n\n            db.session.commit()\n\n            self.assertEqual(len(menu.menu_items), 3)\n            self.assertNotEqual(menu_item1, menu_item2)\n            self.assertNotEqual(menu_item1, menu_item3)\n            self.assertNotEqual(menu_item2, menu_item3)\n            self.assertIn(menu_item1, menu.menu_items)\n            self.assertIn(menu_item2, menu.menu_items)\n            self.assertIn(menu_item3, menu.menu_items)\n\n    def test_get_translation(self):\n        # Test that translation requests are passed through\n\n        with self.app.app_context():\n            trans = self.translator\n\n            db.session.add_all(self.campuses)\n\n            translatable1, _ = self.create_translation({'en': 'Translation 1: en',\n                                                        'nl': 'Translation 1: nl'}, 'en', has_context=True)\n            translatable2, _ = self.create_translation({'en': 'Translation 2: en',\n                                                        'nl': 'Translation 2: nl'}, 'en', has_context=True)\n            translatable3, _ = self.create_translation({'en': 'Translation 3: en',\n                                                        'nl': 'Translation 3: nl'}, 'en', has_context=True)\n\n            menu = Menu.create(self.campuses[0], utils.DAYS['MON'])\n\n            # Required if we need to get menu.id, otherwise it would return None\n            # db.session.flush()\n\n            menu_item1 = menu.add_menu_item(translatable1, CourseType.SUB, CourseSubType.NORMAL,\n                                            [CourseAttributes.SNACK], [], Decimal('1.0'), None)\n            menu_item2 = menu.add_menu_item(translatable2, CourseType.PASTA, CourseSubType.NORMAL,\n                                            [CourseAttributes.PASTA], [], Decimal('1.0'), Decimal('4.0'))\n            menu_item3 = menu.add_menu_item(translatable3, CourseType.SOUP, CourseSubType.VEGAN,\n                                            [CourseAttributes.SOUP], [], Decimal('1.0'), Decimal('2.0'))\n\n            db.session.commit()\n\n            self.assertEqual(menu_item1.get_translation('en', trans), translatable1.get_translation('en', trans))\n            self.assertEqual(menu_item1.get_translation('nl', trans), translatable1.get_translation('nl', trans))\n            self.assertEqual(menu_item1.get_translation('fr', trans), translatable1.get_translation('fr', trans))\n            self.assertEqual(menu_item2.get_translation('en', trans), translatable2.get_translation('en', trans))\n            self.assertEqual(menu_item2.get_translation('nl', trans), translatable2.get_translation('nl', trans))\n            self.assertEqual(menu_item2.get_translation('fr', trans), translatable2.get_translation('fr', trans))\n            self.assertEqual(menu_item3.get_translation('en', trans), translatable3.get_translation('en', trans))\n            self.assertEqual(menu_item3.get_translation('nl', trans), translatable3.get_translation('nl', trans))\n            self.assertEqual(menu_item3.get_translation('fr', trans), translatable3.get_translation('fr', trans))\n"
  },
  {
    "path": "tests/test_models_registered_user.py",
    "content": "import datetime\n\nfrom sqlalchemy import inspect\n\nfrom app import db\nfrom komidabot.models_users import RegisteredUser, Role\nfrom tests.base import BaseTestCase\n\n\nclass TestModelsRegisteredUsers(BaseTestCase):\n    \"\"\"\n    Test models_users.RegisteredUser\n    \"\"\"\n\n    def test_simple_constructors(self):\n        # Test constructor of RegisteredUser model\n\n        with self.app.app_context():\n            user1 = RegisteredUser('test', '123', 'Test User 1', 'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser('test', '456', 'Test User 2', 'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser('test', '789', 'Test User 3', 'user3@example.com', 'https://example.com/img3.png')\n\n            # Ensure that the constructor does not add the entities to the database\n            self.assertTrue(inspect(user1).transient)\n            self.assertTrue(inspect(user2).transient)\n            self.assertTrue(inspect(user3).transient)\n\n            db.session.add(user1)\n            db.session.add(user2)\n            db.session.add(user3)\n\n            db.session.commit()\n\n    # noinspection PyTypeChecker\n    def test_invalid_constructors(self):\n        # Test constructor of RegisteredUser model\n\n        with self.app.app_context():\n            with self.assertRaises(ValueError):\n                RegisteredUser(None, '123', 'Test User 1', 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser(123, '123', 'Test User 1', 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', None, 'Test User 1', 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', 123, 'Test User 1', 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', None, 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', 123, 'user1@example.com', 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', 'Test User 1', None, 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', 'Test User 1', 123, 'https://example.com/img1.png')\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', 'Test User 1', 'user1@example.com', None)\n\n            with self.assertRaises(ValueError):\n                RegisteredUser('test', '123', 'Test User 1', 'user1@example.com', 123)\n\n    def test_create(self):\n        # Test usage of RegisteredUser.create\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            # Ensure that the create method adds the entities to the database\n            self.assertFalse(inspect(user1).transient)\n            self.assertFalse(inspect(user2).transient)\n            self.assertFalse(inspect(user3).transient)\n\n            db.session.commit()\n\n    def test_get_by_id(self):\n        # Test getting a RegisteredUser object by its internal ID\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            db.session.commit()\n\n            self.assertEqual(user1, RegisteredUser.get_by_id(user1.id))\n            self.assertEqual(user2, RegisteredUser.get_by_id(user2.id))\n            self.assertEqual(user3, RegisteredUser.get_by_id(user3.id))\n            self.assertEqual(None, RegisteredUser.get_by_id(user3.id + 1000))\n\n    def test_find_by_provider_id(self):\n        # Test getting a RegisteredUser object by its provider id (subject column)\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            db.session.commit()\n\n            self.assertEqual(user1, RegisteredUser.find_by_provider_id(user1.provider, user1.subject))\n            self.assertEqual(user2, RegisteredUser.find_by_provider_id(user2.provider, user2.subject))\n            self.assertEqual(user3, RegisteredUser.find_by_provider_id(user3.provider, user3.subject))\n            self.assertEqual(None, RegisteredUser.find_by_provider_id('Definitely not used', 'subjectId'))\n\n    def test_find_by_email(self):\n        # Test getting a RegisteredUser object by its email\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            db.session.commit()\n\n            self.assertEqual(user1, RegisteredUser.find_by_email('user1@example.com'))\n            self.assertEqual(user2, RegisteredUser.find_by_email('user2@example.com'))\n            self.assertEqual(user3, RegisteredUser.find_by_email('user3@example.com'))\n\n    def test_get_all(self):\n        # Test getting all RegisteredUser objects\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            db.session.commit()\n\n            users = RegisteredUser.get_all()\n            ids = [user.id for user in users]\n\n            self.assertEqual(len(users), 3)\n            self.assertEqual(len(ids), 3)\n            self.assertIn(user1.id, ids)\n            self.assertIn(user2.id, ids)\n            self.assertIn(user3.id, ids)\n\n    def test_get_all_active(self):\n        # Test getting all active RegisteredUser objects\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            user1.activated_on = datetime.datetime.now()\n            user3.activated_on = user1.activated_on + datetime.timedelta(days=5)\n\n            db.session.commit()\n\n            users = RegisteredUser.get_all_active()\n            ids = [user.id for user in users]\n\n            self.assertEqual(len(users), 2)\n            self.assertEqual(len(ids), 2)\n            self.assertIn(user1.id, ids)\n            self.assertNotIn(user2.id, ids)\n            self.assertIn(user3.id, ids)\n\n    def test_get_all_by_role(self):\n        # Test getting all active RegisteredUser objects\n\n        with self.app.app_context():\n            role1 = Role.create('test_role1')\n            role2 = Role.create('test_role2')\n\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            user1.add_role(role1)\n            user2.add_role(role1)\n            user2.add_role(role2)\n\n            db.session.commit()\n\n            users1 = RegisteredUser.get_all_by_role(role1)\n            ids1 = [user.id for user in users1]\n\n            self.assertEqual(len(users1), 2)\n            self.assertEqual(len(ids1), 2)\n            self.assertIn(user1.id, ids1)\n            self.assertIn(user2.id, ids1)\n            self.assertNotIn(user3.id, ids1)\n\n            users2 = RegisteredUser.get_all_by_role(role2)\n            ids2 = [user.id for user in users2]\n\n            self.assertEqual(len(users2), 1)\n            self.assertEqual(len(ids2), 1)\n            self.assertNotIn(user1.id, ids2)\n            self.assertIn(user2.id, ids2)\n            self.assertNotIn(user3.id, ids2)\n\n    def test_roles(self):\n        # Test getting all active RegisteredUser objects\n\n        with self.app.app_context():\n            role1 = Role.create('test_role1')\n            role2 = Role.create('test_role2')\n\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            user1.add_role(role1)\n            user2.add_role(role1)\n            user2.add_role(role2)\n\n            db.session.commit()\n\n            self.assertTrue(user1.is_role(role1))\n            self.assertTrue(user2.is_role(role1))\n            self.assertFalse(user3.is_role(role1))\n            self.assertFalse(user1.is_role(role2))\n            self.assertTrue(user2.is_role(role2))\n            self.assertFalse(user3.is_role(role2))\n\n            user2.remove_role(role1)\n\n            self.assertTrue(user1.is_role(role1.name))\n            self.assertFalse(user2.is_role(role1.name))\n            self.assertFalse(user3.is_role(role1.name))\n            self.assertFalse(user1.is_role(role2.name))\n            self.assertTrue(user2.is_role(role2.name))\n            self.assertFalse(user3.is_role(role2.name))\n\n    def test_delete(self):\n        # Test getting all RegisteredUser objects\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user3 = RegisteredUser.create('test', '789', 'Test User 3',\n                                          'user3@example.com', 'https://example.com/img3.png')\n\n            db.session.commit()\n\n            user2.delete()\n\n            db.session.commit()\n\n            users = RegisteredUser.get_all()\n            ids = [user.id for user in users]\n\n            self.assertEqual(len(users), 2)\n            self.assertEqual(len(ids), 2)\n            self.assertIn(user1.id, ids)\n            self.assertNotIn(user2.id, ids)\n            self.assertIn(user3.id, ids)\n\n    def test_user_mixin(self):\n        # Test getting all RegisteredUser objects\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user2.activated_on = datetime.datetime.now()\n\n            db.session.commit()\n\n            self.assertIsNone(user1.activated_on)\n            self.assertFalse(user1.is_active)\n            self.assertFalse(user1.is_anonymous)\n\n            self.assertIsNotNone(user2.activated_on)\n            self.assertTrue(user2.is_active)\n            self.assertFalse(user2.is_anonymous)\n\n    def test_subscriptions(self):\n        # Test getting all RegisteredUser objects\n\n        with self.app.app_context():\n            user1 = RegisteredUser.create('test', '123', 'Test User 1',\n                                          'user1@example.com', 'https://example.com/img1.png')\n            user2 = RegisteredUser.create('test', '456', 'Test User 2',\n                                          'user2@example.com', 'https://example.com/img2.png')\n            user1.activated_on = user2.activated_on = datetime.datetime.now()\n\n            db.session.commit()\n\n            # Assert that we start with no subscriptions\n            self.assertEqual(len(user1.get_subscriptions()), 0)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Add a subscription to user 1\n            user1.add_subscription('https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8', {'key1': 'value1'})\n\n            # Assert adding a subscription to one user doesn't add one to another\n            self.assertEqual(len(user1.get_subscriptions()), 1)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that the added subscription is the one we put in\n            self.assertIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                           'keys': {'key1': 'value1'}},\n                          user1.get_subscriptions())\n\n            # Also check that we're not breaking time I guess\n            self.assertNotIn({'endpoint': 'https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                              'keys': {'key2': 'value2'}},\n                             user1.get_subscriptions())\n\n            # Add a 2nd subscription to user 1\n            user1.add_subscription('https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2', {'key2': 'value2'})\n\n            # Assert adding a subscription to one user doesn't add one to another once more\n            self.assertEqual(len(user1.get_subscriptions()), 2)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that all the subscriptions we added are in there\n            self.assertIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                           'keys': {'key1': 'value1'}},\n                          user1.get_subscriptions())\n            self.assertIn({'endpoint': 'https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                           'keys': {'key2': 'value2'}},\n                          user1.get_subscriptions())\n\n            # Add the 2nd subscription to user 1 once more\n            user1.add_subscription('https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2', {'key2': 'value2'})\n\n            # Assert adding a subscription to one user doesn't add one to another once more\n            self.assertEqual(len(user1.get_subscriptions()), 2)  # Length unchanged, no duplicates allowed\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that all the subscriptions we added are in there\n            self.assertIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                           'keys': {'key1': 'value1'}},\n                          user1.get_subscriptions())\n            self.assertIn({'endpoint': 'https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                           'keys': {'key2': 'value2'}},\n                          user1.get_subscriptions())\n\n            # Try to add the 2nd subscription to user 1, but with different keys\n            user1.add_subscription('https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2', {'key10': 'value10'})\n\n            # Assert that this did not add a new subscription\n            self.assertEqual(len(user1.get_subscriptions()), 2)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that the subscriptions are unchanged by this\n            self.assertIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                           'keys': {'key1': 'value1'}},\n                          user1.get_subscriptions())\n            self.assertIn({'endpoint': 'https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                           'keys': {'key2': 'value2'}},\n                          user1.get_subscriptions())\n\n            # Try to remove the 1st subscription from user 2\n            user2.remove_subscription('https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8')\n\n            # Assert that this did nothing\n            self.assertEqual(len(user1.get_subscriptions()), 2)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Remove the 1st subscription from user 1\n            user1.remove_subscription('https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8')\n\n            # Assert that this time stuff actually happened\n            self.assertEqual(len(user1.get_subscriptions()), 1)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that only the subscription we removed was actually removed\n            self.assertNotIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                              'keys': {'key1': 'value1'}},\n                             user1.get_subscriptions())\n            self.assertIn({'endpoint': 'https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                           'keys': {'key2': 'value2'}},\n                          user1.get_subscriptions())\n\n            # Bring back the 1st subscription\n            user1.add_subscription('https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8', {'key1': 'value1'})\n\n            # Replace the 2nd subscription\n            user1.replace_subscription('https://example.com/2ee246b0-c8b2-4b6c-a08d-acb26ab392d2',\n                                       'https://example.com/4c991903-c193-447b-ac5b-b3b8674cd5f9',\n                                       {'key3': 'value3'})\n\n            # Assert that no additional subscriptions were added, only modified\n            self.assertEqual(len(user1.get_subscriptions()), 2)\n            self.assertEqual(len(user2.get_subscriptions()), 0)\n\n            # Assert that the 2nd subscription was replaced\n            self.assertIn({'endpoint': 'https://example.com/6cc32b91-6938-4d8b-9f3b-f0fccc015ca8',\n                           'keys': {'key1': 'value1'}},\n                          user1.get_subscriptions())\n            self.assertIn({'endpoint': 'https://example.com/4c991903-c193-447b-ac5b-b3b8674cd5f9',\n                           'keys': {'key3': 'value3'}},\n                          user1.get_subscriptions())\n"
  },
  {
    "path": "tests/test_models_translations.py",
    "content": "from sqlalchemy import inspect\n\nimport komidabot.models as models\nfrom app import db\nfrom tests.base import BaseTestCase\n\n\n# TODO: Add provider tests\nclass TestModelsTranslations(BaseTestCase):\n    \"\"\"\n    Test models.Translatable and models.Translation\n    \"\"\"\n\n    def test_simple_constructors(self):\n        # Test constructor of Translatable and Translation models\n\n        with self.app.app_context():\n            translatable1 = models.Translatable('Translation 1: en', 'en')\n            translatable2 = models.Translatable('Translation 2: en', 'en')\n            translatable3 = models.Translatable('Translation 3: en', 'en')\n\n            # Ensure that the constructor does not add the entities to the database\n            self.assertTrue(inspect(translatable1).transient)\n            self.assertTrue(inspect(translatable2).transient)\n            self.assertTrue(inspect(translatable3).transient)\n\n            db.session.add(translatable1)\n            db.session.add(translatable2)\n            db.session.add(translatable3)\n\n            db.session.flush()\n\n            translation1a = models.Translation(translatable1.id, 'nl', 'Translation 1: nl')\n            translation1b = models.Translation(translatable1.id, 'fr', 'Translation 1: fr')\n\n            self.assertTrue(inspect(translation1a).transient)\n            self.assertTrue(inspect(translation1b).transient)\n\n            db.session.add(translatable1)\n            db.session.add(translatable2)\n\n            db.session.commit()\n\n            translation2 = models.Translation(translatable2.id, 'nl', 'Translation 2: nl')\n\n            self.assertTrue(inspect(translation2).transient)\n\n            db.session.add(translation2)\n\n            db.session.commit()\n\n    def test_get_or_create(self):\n        # Test usage of Translatable.get_or_create\n\n        with self.app.app_context():\n            translatable1, translation1 = models.Translatable.get_or_create('Translation 1: en', 'en')\n            translatable2, translation2 = models.Translatable.get_or_create('Translation 2: en', 'en')\n            translatable3, translation3 = models.Translatable.get_or_create('Translation 3: en', 'en')\n\n            # Ensure that the create method adds the entities to the database\n            self.assertFalse(inspect(translatable1).transient)\n            self.assertFalse(inspect(translation1).transient)\n            self.assertFalse(inspect(translatable2).transient)\n            self.assertFalse(inspect(translation2).transient)\n            self.assertFalse(inspect(translatable3).transient)\n            self.assertFalse(inspect(translation3).transient)\n\n            db.session.commit()\n\n    def test_add_translation(self):\n        # Test usage of Translatable.add_translation\n\n        with self.app.app_context():\n            translatable1, translation1a = models.Translatable.get_or_create('Translation 1: en', 'en')\n            translatable2, translation2a = models.Translatable.get_or_create('Translation 2: en', 'en')\n            translatable3, translation3a = models.Translatable.get_or_create('Translation 3: en', 'en')\n\n            translation1b = translatable1.add_translation('nl', 'Translation 1: nl')\n\n            db.session.flush()\n\n            translation2b = translatable2.add_translation('nl', 'Translation 2: nl')\n\n            db.session.commit()\n\n            translation3b = translatable3.add_translation('nl', 'Translation 3: nl')\n\n            db.session.commit()\n\n            translations1 = translatable1.translations\n            translations2 = translatable2.translations\n            translations3 = translatable3.translations\n\n            self.assertEqual(len(translations1), 2)\n            self.assertEqual(len(translations2), 2)\n            self.assertEqual(len(translations3), 2)\n            self.assertIn(translation1a, translations1)\n            self.assertIn(translation1b, translations1)\n            self.assertIn(translation2a, translations2)\n            self.assertIn(translation2b, translations2)\n            self.assertIn(translation3a, translations3)\n            self.assertIn(translation3b, translations3)\n\n    def test_has_translation(self):\n        # Test usage of Translatable.add_translation\n\n        with self.app.app_context():\n            translatable1, translation1a = models.Translatable.get_or_create('Translation 1: en', 'en')\n            translatable2, translation2a = models.Translatable.get_or_create('Translation 2: nl', 'nl')\n\n            translatable1.add_translation('nl', 'Translation 1: nl')\n\n            db.session.flush()\n\n            translatable2.add_translation('en', 'Translation 2: en')\n\n            db.session.commit()\n\n            self.assertTrue(translatable1.has_translation('en'))\n            self.assertTrue(translatable2.has_translation('en'))\n            self.assertTrue(translatable1.has_translation('nl'))\n            self.assertTrue(translatable2.has_translation('nl'))\n            self.assertFalse(translatable1.has_translation('fr'))\n            self.assertFalse(translatable2.has_translation('fr'))\n\n    # TODO: Test get_translation\n    def test_get_translation(self):\n        # Test usage of Translatable.get_translation\n\n        with self.app.app_context():\n            translatable1, translation1a = models.Translatable.get_or_create('Translation 1: en', 'en')\n            translatable2, translation2a = models.Translatable.get_or_create('Translation 2: en', 'en')\n            translatable3, translation3a = models.Translatable.get_or_create('Translation 3: en', 'en')\n\n            translation1b = translatable1.add_translation('nl', 'Translation 1: nl')\n            translation2b = translatable2.add_translation('nl', 'Translation 2: nl')\n            translation3b = translatable3.add_translation('nl', 'Translation 3: nl')\n\n            db.session.commit()\n\n            self.assertEqual(translatable1.get_translation('en', None), translation1a)\n            self.assertEqual(translatable2.get_translation('en', None), translation2a)\n            self.assertEqual(translatable3.get_translation('en', None), translation3a)\n            self.assertEqual(translatable1.get_translation('nl', None), translation1b)\n            self.assertEqual(translatable2.get_translation('nl', None), translation2b)\n            self.assertEqual(translatable3.get_translation('nl', None), translation3b)\n\n            translation1c = translatable1.get_translation('fr', self.translator)\n            translation2c = translatable2.get_translation('fr', self.translator)\n            translation3c = translatable3.get_translation('fr', self.translator)\n\n            db.session.commit()\n\n            self.assertNotEqual(translation1c, translation1a)\n            self.assertNotEqual(translation1c, translation1b)\n            self.assertNotEqual(translation2c, translation2a)\n            self.assertNotEqual(translation2c, translation2b)\n            self.assertNotEqual(translation3c, translation3a)\n            self.assertNotEqual(translation3c, translation3b)\n\n            for translation in [translation1c, translation2c, translation3c]:\n                translatable: models.Translatable = translation.translatable\n\n                self.assertEqual(translation.translation, self.translator.translate(translatable.original_text,\n                                                                                    translatable.original_language,\n                                                                                    translation.language))\n\n    def test_get_by_id(self):\n        # Test usage of Translatable.get_by_id\n\n        with self.app.app_context():\n            translatable1, _ = models.Translatable.get_or_create('Translation 1: en', 'en')\n            translatable2, _ = models.Translatable.get_or_create('Translation 2: en', 'en')\n            translatable3, _ = models.Translatable.get_or_create('Translation 3: en', 'en')\n\n            db.session.commit()\n\n            self.assertEqual(translatable1, models.Translatable.get_by_id(translatable1.id))\n            self.assertEqual(translatable2, models.Translatable.get_by_id(translatable2.id))\n            self.assertEqual(translatable3, models.Translatable.get_by_id(translatable3.id))\n"
  },
  {
    "path": "tests/test_subscriptions.py",
    "content": "import datetime\nfrom decimal import Decimal\nfrom typing import Dict, List, Tuple\n\nimport komidabot.models as models\nimport komidabot.triggers as triggers\nimport komidabot.users as users\nimport komidabot.util as util\nimport tests.users_stub as users_stub\nimport tests.utils as utils\nfrom app import db\nfrom komidabot.models import AppUser, Day, CourseType, CourseSubType, UserDayCampusPreference, course_icons_matrix\nfrom tests.base import BaseTestCase, HttpCapture, menu_item\n\n\nclass BaseSubscriptionsTestCase(BaseTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.create_test_campuses()\n\n\nclass TestGenericSubscriptions(BaseSubscriptionsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        with self.app.app_context():\n            user_manager = users_stub.UserManager()\n            self.message_handler = user_manager.message_handler\n            self.app.user_manager = user_manager  # Replace the unified user manager completely to test\n\n            self.user1 = user_manager.add_user('user1', locale='nl')\n            self.user2 = user_manager.add_user('user2', locale='nl')\n            self.user3 = user_manager.add_user('user3', locale='nl')\n\n            db.session.commit()\n\n    def setup_subscriptions(self):\n        def create_subscriptions(user: users.UserId, days: List[Tuple[Day, int, bool]]):\n            for day, campus, active in days:\n                user_obj = AppUser.find_by_id(user.provider, user.id)\n                UserDayCampusPreference.create(user_obj, day, self.campuses[campus], active=active)\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            # First user, subscribed every day\n            create_subscriptions(self.user1.id, [\n                (Day.MONDAY, 0, True),\n                (Day.TUESDAY, 1, True),\n                (Day.WEDNESDAY, 0, True),\n                (Day.THURSDAY, 1, True),\n                (Day.FRIDAY, 0, True),\n            ])\n\n            # Second user, always goes on tuesdays and thursdays, otherwise sporadically\n            create_subscriptions(self.user2.id, [\n                (Day.MONDAY, 1, False),\n                (Day.TUESDAY, 1, True),\n                (Day.WEDNESDAY, 1, False),\n                (Day.THURSDAY, 0, True),\n                (Day.FRIDAY, 0, False),\n            ])\n\n            # Third user, only comes when he wants to\n            create_subscriptions(self.user3.id, [\n                (Day.MONDAY, 0, False),\n                (Day.TUESDAY, 0, False),\n                (Day.WEDNESDAY, 1, False),\n                (Day.THURSDAY, 1, False),\n                (Day.FRIDAY, 1, False),\n            ])\n\n            db.session.commit()\n\n    def setup_menu(self):\n        self.expected_menus: Dict[Tuple[str, datetime.date], str] = dict()\n        course_types = CourseType\n        sub_types = CourseSubType\n\n        with self.app.app_context():\n            db.session.add_all(self.campuses)\n\n            for campus in self.campuses:\n                for day in utils.DAYS_LIST:\n                    day_name = Day(day.isoweekday()).name\n                    items = [menu_item(course_type,\n                                       sub_type,\n                                       [],\n                                       [],\n                                       '{} at {} for {}'.format(course_type.name, campus.short_name, day_name),\n                                       'nl',\n                                       Decimal('1.0'),\n                                       Decimal('2.0'))\n                             for course_type in course_types\n                             for sub_type in sub_types]\n                    self.create_menu(campus, day, items, has_context=True)\n\n                    result = [\n                        'Menu van {date} in {campus}'.format(campus=campus.name,\n                                                             date=util.date_to_string('nl', day)),\n                        '',\n                    ]\n                    for item in items:\n                        result.append('{} {} ({} / {})'.format(course_icons_matrix[item.type][item.sub_type], item.text,\n                                                               models.MenuItem.format_price(item.price_students),\n                                                               models.MenuItem.format_price(item.price_staff)))\n\n                    self.expected_menus[(campus.short_name, day)] = '\\n'.join(result)\n\n            db.session.commit()\n\n    def test_active_subscriptions(self):\n        self.setup_subscriptions()\n        self.setup_menu()\n\n        with self.app.app_context():\n            self.activate_feature('menu_subscription', available=True, has_context=True)\n\n            with HttpCapture():  # Ensure no requests are made\n                self.app.bot.trigger_received(triggers.SubscriptionTrigger(date=utils.DAYS['MON']))\n                self.app.bot.trigger_received(triggers.SubscriptionTrigger(date=utils.DAYS['TUE']))\n                self.app.bot.trigger_received(triggers.SubscriptionTrigger(date=utils.DAYS['WED']))\n                self.app.bot.trigger_received(triggers.SubscriptionTrigger(date=utils.DAYS['THU']))\n                self.app.bot.trigger_received(triggers.SubscriptionTrigger(date=utils.DAYS['FRI']))\n\n                db.session.add_all(self.campuses)\n\n                self.assertIn(self.user1.id, self.message_handler.message_log)\n                self.assertEqual(self.message_handler.message_log[self.user1.id], [\n                    self.expected_menus[(self.campuses[0].short_name, utils.DAYS['MON'])],\n                    self.expected_menus[(self.campuses[1].short_name, utils.DAYS['TUE'])],\n                    self.expected_menus[(self.campuses[0].short_name, utils.DAYS['WED'])],\n                    self.expected_menus[(self.campuses[1].short_name, utils.DAYS['THU'])],\n                    self.expected_menus[(self.campuses[0].short_name, utils.DAYS['FRI'])],\n                ])\n\n                self.assertIn(self.user2.id, self.message_handler.message_log)\n                self.assertEqual(self.message_handler.message_log[self.user2.id], [\n                    self.expected_menus[(self.campuses[1].short_name, utils.DAYS['TUE'])],\n                    self.expected_menus[(self.campuses[0].short_name, utils.DAYS['THU'])],\n                ])\n\n                self.assertNotIn(self.user3.id, self.message_handler.message_log)\n\n                # print(self.message_handler.message_log, flush=True)\n\n\n# class TestFacebookSubscriptions(BaseSubscriptionsTestCase):\n#     def test_http_capture(self):\n#         with self.app.app_context():\n#             with HttpCapture() as http:\n#                 http.register_uri(HttpCapture.GET, 'https://google.be', 'test')\n#\n#                 response = requests.get('https://google.be')\n#\n#                 assert response.text == 'test'\n"
  },
  {
    "path": "tests/test_test_utils.py",
    "content": "import tests.utils as utils\nfrom tests.base import BaseTestCase\n\n\nclass TestConstants(BaseTestCase):\n    \"\"\"\n    Sanity tests for testing utilities.\n    \"\"\"\n\n    def test_days(self):\n        self.assertEqual(utils.DAYS['MON'].isoweekday(), 1, 'Date is not a Monday')\n        self.assertEqual(utils.DAYS['TUE'].isoweekday(), 2, 'Date is not a Tuesday')\n        self.assertEqual(utils.DAYS['WED'].isoweekday(), 3, 'Date is not a Wednesday')\n        self.assertEqual(utils.DAYS['THU'].isoweekday(), 4, 'Date is not a Thursday')\n        self.assertEqual(utils.DAYS['FRI'].isoweekday(), 5, 'Date is not a Friday')\n        self.assertEqual(utils.DAYS['SAT'].isoweekday(), 6, 'Date is not a Saturday')\n        self.assertEqual(utils.DAYS['SUN'].isoweekday(), 7, 'Date is not a Sunday')\n\n    def test_days_list(self):\n        self.assertEqual(utils.DAYS_LIST[0].isoweekday(), 1, 'Date is not a Monday')\n        self.assertEqual(utils.DAYS_LIST[1].isoweekday(), 2, 'Date is not a Tuesday')\n        self.assertEqual(utils.DAYS_LIST[2].isoweekday(), 3, 'Date is not a Wednesday')\n        self.assertEqual(utils.DAYS_LIST[3].isoweekday(), 4, 'Date is not a Thursday')\n        self.assertEqual(utils.DAYS_LIST[4].isoweekday(), 5, 'Date is not a Friday')\n        self.assertEqual(utils.DAYS_LIST[5].isoweekday(), 6, 'Date is not a Saturday')\n        self.assertEqual(utils.DAYS_LIST[6].isoweekday(), 7, 'Date is not a Sunday')\n"
  },
  {
    "path": "tests/test_triggers.py",
    "content": "import datetime\n\nimport komidabot.triggers as triggers\nfrom tests.base import BaseTestCase\nfrom tests.users_stub import UserManager as TestUserManager\n\n\nclass TestTriggers(BaseTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.user_manager = TestUserManager()\n        self.user1 = self.user_manager.add_user('user1')\n\n        self.triggers = [\n            (triggers.Trigger, (), {}),\n            (triggers.TextTrigger, ('Hello world!',), {}),\n            (triggers.SubscriptionTrigger, (), {}),\n            (triggers.SubscriptionTrigger, (), {'date': datetime.date(1999, 12, 31)}),\n        ]\n\n        self.aspects = [\n            (triggers.SenderAspect, (self.user1,), {}),\n            (triggers.DatetimeAspect, ('1999-12-31T00:00:00.000+01:00', 'day',), {}),\n            (triggers.LocaleAspect, ('nl_XX', 1.0,), {}),\n        ]\n\n    def test_simple_trigger_constructors(self):\n        # Test constructors of Trigger and classes extending Trigger without any Aspects\n\n        for TriggerType, args, kwargs in self.triggers:\n            TriggerType(*args, **kwargs)\n\n    def test_trigger_constructors_with_aspects(self):\n        # Test constructors of Trigger and classes extending Trigger with Aspects\n        pass\n\n        # for TriggerType, args, kwargs in self.triggers:\n        #     TriggerType(*args, **kwargs)\n\n    def test_aspect_constructors(self):\n        # Test constructors of classes extending Aspect\n\n        for AspectType, args, kwargs in self.aspects:\n            AspectType(*args, **kwargs)\n\n    def test_simple_extend(self):\n        # Test the extend method of Trigger and classes extending Trigger without any Aspects\n\n        trigger = triggers.Trigger()\n        self.assertIsInstance(trigger, triggers.Trigger)\n\n        for TriggerType, args, kwargs in self.triggers:\n            trigger = TriggerType.extend(trigger, *args, **kwargs)\n            self.assertIsInstance(trigger, TriggerType)\n\n    def test_extend_with_aspects(self):\n        # Test the extend method of Trigger and classes extending Trigger with Aspects\n        pass\n\n        # trigger = triggers.Trigger()\n        # self.assertIsInstance(trigger, triggers.Trigger)\n        #\n        # for TriggerType, args, kwargs in self.triggers:\n        #     trigger = TriggerType.extend(trigger, *args, **kwargs)\n        #     self.assertIsInstance(trigger, TriggerType)\n\n    def test_no_aspects(self):\n        # Test that the 'in' operator does not falsely report Aspects inside Triggers\n\n        for TriggerType, args, kwargs in self.triggers:\n            trigger = TriggerType(*args, **kwargs)\n\n            self.assertNotIn(triggers.Aspect, trigger)\n            self.assertNotIn(triggers.SenderAspect, trigger)\n            self.assertNotIn(triggers.DatetimeAspect, trigger)\n            self.assertNotIn(triggers.LocaleAspect, trigger)\n\n    def test_single_aspect(self):\n        # Test that the 'in' operator does not falsely report Aspects inside Triggers if there is another type\n\n        for TriggerType, args, kwargs in self.triggers:\n            for AspectType, args2, kwargs2 in self.aspects:\n                aspect = AspectType(*args2, **kwargs2)\n                trigger = TriggerType(*args, aspects=[aspect, ], **kwargs)\n\n                self.assertIn(AspectType, trigger)\n                self.assertEqual(trigger[AspectType], [aspect, ] if AspectType.allows_multiple else aspect)\n\n                for AspectType2, _, _ in self.aspects:\n                    if AspectType2 is AspectType:\n                        continue  # This is the type we're testing in this instance\n\n                    self.assertNotIn(AspectType2, trigger)\n\n    def test_multiple_aspects(self):\n        # Test that the 'in' operator does not falsely report Aspects not inside Triggers if there are others as well\n\n        for TriggerType, args, kwargs in self.triggers:\n            for AspectType, args2, kwargs2 in self.aspects:\n                for AspectType2, args3, kwargs3 in self.aspects:\n                    if AspectType is AspectType2:\n                        continue  # This Aspect type is already in the Trigger\n\n                    aspect1 = AspectType(*args2, **kwargs2)\n                    aspect2 = AspectType2(*args3, **kwargs3)\n                    trigger = TriggerType(*args, aspects=[aspect1, aspect2, ], **kwargs)\n\n                    self.assertIn(AspectType, trigger)\n                    self.assertEqual(trigger[AspectType], [aspect1, ] if AspectType.allows_multiple else aspect1)\n                    self.assertIn(AspectType2, trigger)\n                    self.assertEqual(trigger[AspectType2], [aspect2, ] if AspectType2.allows_multiple else aspect2)\n\n                    for AspectType3, _, _ in self.aspects:\n                        if AspectType3 is AspectType3 or AspectType3 is AspectType2:\n                            continue  # This is the type we're testing in this instance\n\n                        self.assertNotIn(AspectType3, trigger)\n"
  },
  {
    "path": "tests/test_users_base.py",
    "content": "import tests.users_stub as users_stub\nfrom app import db\nfrom komidabot.users import UserId\nfrom tests.base import BaseTestCase\n\n\nclass TestUsersBase(BaseTestCase):\n    \"\"\"\n    Base tests for komidabot.users\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        with self.app.app_context():\n            user_manager = users_stub.UserManager()\n            self.app.user_manager.register_manager(user_manager)\n\n            self.app.admin_ids = [UserId('admin1', users_stub.PROVIDER_ID), UserId('admin2', users_stub.PROVIDER_ID)]\n\n            self.user1 = user_manager.add_user('user1', locale='nl')\n            self.user2 = user_manager.add_user('user2', locale='nl')\n\n            # Defined in TestingConfig\n            self.admin1 = user_manager.add_user('admin1', locale='nl')\n            self.admin2 = user_manager.add_user('admin2', locale='nl')\n\n            db.session.commit()\n\n    def test_get_administrators(self):\n        with self.app.app_context():\n            administrators = self.app.user_manager.get_administrators()\n\n            self.assertEqual(len(administrators), 2)\n            self.assertNotIn(self.user1, administrators)\n            self.assertNotIn(self.user2, administrators)\n            self.assertIn(self.admin1, administrators)\n            self.assertIn(self.admin2, administrators)\n"
  },
  {
    "path": "tests/users_stub.py",
    "content": "from typing import Dict, List\nfrom typing import Union\n\nimport komidabot.menu\nimport komidabot.messages as messages\nimport komidabot.users as users\nfrom komidabot.models import AppUser, Menu\nfrom komidabot.subscriptions.daily_menu import CHANNEL_ID as DAILY_MENU_ID\n\nPROVIDER_ID = 'stub'\n\n\nclass UserManager(users.UserManager):\n    def __init__(self):\n        self.users: 'Dict[users.UserId, User]' = dict()\n\n        self.message_handler = MessageHandler()\n\n    def add_user(self, internal_id: str, locale: str = 'nl') -> 'User':\n        user_id = users.UserId(internal_id, PROVIDER_ID)\n\n        if user_id in self.users:\n            raise ValueError('Duplicate user ID')\n\n        user = User(self, user_id.id)\n        self.users[user_id] = user\n\n        user.add_to_db()\n        user.get_db_user().set_language(locale)\n\n        return user\n\n    def get_user(self, user: 'Union[users.UserId, AppUser]', **kwargs) -> 'User':\n        if isinstance(user, AppUser):\n            user = users.UserId(user.internal_id, user.provider)\n\n        if not isinstance(user, users.UserId):\n            raise ValueError()\n\n        if user not in self.users:\n            raise ValueError('Invalid user ID: {}'.format(user))\n\n        return self.users[user]\n\n    def initialise(self):\n        assert False  # Does not get called\n\n    def get_identifier(self):\n        return PROVIDER_ID\n\n\nclass User(users.User):\n    def __init__(self, manager: UserManager, internal_id: str):\n        self._manager = manager\n        self._id = internal_id\n\n    def get_provider_name(self) -> 'str':\n        return PROVIDER_ID\n\n    def get_internal_id(self) -> 'str':\n        return self._id\n\n    def supports_subscription_channel(self, channel: str) -> bool:\n        return channel in [DAILY_MENU_ID]\n\n    def get_manager(self) -> UserManager:\n        return self._manager\n\n    def get_message_handler(self):\n        if self._manager.message_handler is None:\n            raise NotImplementedError()\n        return self._manager.message_handler\n\n\nclass MessageHandler(messages.MessageHandler):\n    \"\"\"Message handler that stores messages in a user->messages dictionary\"\"\"\n\n    def __init__(self):\n        self.message_log: Dict[users.UserId, List[str]] = dict()\n\n    def reset(self):\n        self.message_log = dict()\n\n    def send_message(self, user, message: messages.Message) -> messages.MessageSendResult:\n        if user.id.provider != PROVIDER_ID:\n            raise ValueError('User id is not for Stub Provider')\n\n        if isinstance(message, messages.TextMessage):\n            if user.id not in self.message_log:\n                self.message_log[user.id] = []\n\n            text = message.text\n\n            self.message_log[user.id].append(text)\n\n            return messages.MessageSendResult.SUCCESS\n        elif isinstance(message, messages.MenuMessage):\n            if user.id not in self.message_log:\n                self.message_log[user.id] = []\n\n            text = komidabot.menu.get_menu_text(message.menu, message.translator, user.get_locale())\n\n            self.message_log[user.id].append(text)\n\n            return messages.MessageSendResult.SUCCESS\n        elif isinstance(message, messages.SubscriptionMenuMessage):\n            if user.id not in self.message_log:\n                self.message_log[user.id] = []\n\n            campus = user.get_campus_for_day(message.date)\n            menu = Menu.get_menu(campus, message.date)\n\n            text = komidabot.menu.get_menu_text(menu, message.translator, user.get_locale())\n\n            self.message_log[user.id].append(text)\n\n            return messages.MessageSendResult.SUCCESS\n        else:\n            return messages.MessageSendResult.UNSUPPORTED\n"
  },
  {
    "path": "tests/utils.py",
    "content": "import datetime\n\nimport komidabot.translation as translation\n\nDAYS = {\n    'MON': datetime.date(2019, 7, 1),\n    'TUE': datetime.date(2019, 7, 2),\n    'WED': datetime.date(2019, 7, 3),\n    'THU': datetime.date(2019, 7, 4),\n    'FRI': datetime.date(2019, 7, 5),\n    'SAT': datetime.date(2019, 7, 6),\n    'SUN': datetime.date(2019, 7, 7),\n}\n\nDAYS_LIST = list(DAYS.values())\n\n\nclass StubTranslator(translation.TranslationService):\n    def translate(self, text: str, from_language: translation.Language, to_language: translation.Language):\n        return 'No translation {}: {} -> {}'.format(repr(text), from_language, to_language)\n\n    @property\n    def identifier(self):\n        return 'stub'\n\n    @property\n    def pretty_name(self):\n        return 'Stub translator implementation'\n"
  },
  {
    "path": "wait-postgres.sh",
    "content": "#!/usr/bin/env /bin/bash\n\necho \"Waiting for postgres...\"\n\n# Wait for the database in a safe manner\nwhile :\ndo\n    trap 'kill -TERM $PID' TERM INT\n    nc -w 2 -z \"$POSTGRES_HOST\" 5432 &\n    PID=$!\n    wait $PID\n    trap - TERM INT\n    wait $PID\n    EXIT_STATUS=$?\n\n    if [[ ${EXIT_STATUS} -eq 0 ]]\n    then\n        break\n    fi\n\n    sleep 0.1\ndone\n\necho \"PostgreSQL started\"\n"
  }
]