Skip to content

Instantly share code, notes, and snippets.

@ianchanning
Created October 18, 2020 09:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ianchanning/201cb323e9ae90aec06c37b719f2dac1 to your computer and use it in GitHub Desktop.
Save ianchanning/201cb323e9ae90aec06c37b719f2dac1 to your computer and use it in GitHub Desktop.
coc.nvim debug log
This file has been truncated, but you can view the full file.
11:54:54 DEBUG [transport] - request to vim: -1,nvim_get_api_info,[]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"get_api_info",
[]
],
-1
]
11:54:54 DEBUG [transport] - request to vim: -2,nvim_call_function,[
"coc#util#path_replace_patterns",
[]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#path_replace_patterns",
[]
]
],
-2
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"set_var",
[
"coc_process_pid",
17368
]
]
]
11:54:54 DEBUG [connection] - received notification: [
"VimEnter",
[]
]
11:54:54 DEBUG [connection] - received request: 1,[
"extensionStats",
[]
]
11:54:54 DEBUG [connection] - received response: -1,[
null,
[
1,
{
"functions": [
{
"name": "nvim_buf_attach"
},
{
"name": "nvim_get_mode"
},
{
"name": "nvim_list_runtime_paths"
},
{
"name": "nvim_win_del_var"
},
{
"name": "nvim_tabpage_list_wins"
},
{
"name": "nvim_buf_del_var"
},
{
"name": "nvim_buf_get_mark"
},
{
"name": "nvim_tabpage_set_var"
},
{
"name": "nvim_create_namespace"
},
{
"name": "nvim_win_get_position"
},
{
"name": "nvim_win_set_height"
},
{
"name": "nvim_call_atomic"
},
{
"name": "nvim_buf_detach"
},
{
"name": "nvim_buf_line_count"
},
{
"name": "nvim_set_current_buf"
},
{
"name": "nvim_set_current_dir"
},
{
"name": "nvim_get_var"
},
{
"name": "nvim_del_current_line"
},
{
"name": "nvim_win_set_width"
},
{
"name": "nvim_out_write"
},
{
"name": "nvim_win_is_valid"
},
{
"name": "nvim_set_current_win"
},
{
"name": "nvim_get_current_tabpage"
},
{
"name": "nvim_tabpage_is_valid"
},
{
"name": "nvim_set_var"
},
{
"name": "nvim_win_get_height"
},
{
"name": "nvim_win_get_buf"
},
{
"name": "nvim_win_get_width"
},
{
"name": "nvim_buf_set_name"
},
{
"name": "nvim_subscribe"
},
{
"name": "nvim_get_current_win"
},
{
"name": "nvim_feedkeys"
},
{
"name": "nvim_get_vvar"
},
{
"name": "nvim_tabpage_get_number"
},
{
"name": "nvim_get_current_buf"
},
{
"name": "nvim_win_get_option"
},
{
"name": "nvim_win_get_cursor"
},
{
"name": "nvim_get_current_line"
},
{
"name": "nvim_win_get_var"
},
{
"name": "nvim_buf_get_var"
},
{
"name": "nvim_set_current_tabpage"
},
{
"name": "nvim_buf_clear_namespace"
},
{
"name": "nvim_err_write"
},
{
"name": "nvim_del_var"
},
{
"name": "nvim_call_dict_function"
},
{
"name": "nvim_set_current_line"
},
{
"name": "nvim_get_api_info"
},
{
"name": "nvim_unsubscribe"
},
{
"name": "nvim_get_option"
},
{
"name": "nvim_list_wins"
},
{
"name": "nvim_set_client_info"
},
{
"name": "nvim_win_set_cursor"
},
{
"name": "nvim_win_set_option"
},
{
"name": "nvim_eval"
},
{
"name": "nvim_tabpage_get_var"
},
{
"name": "nvim_buf_get_option"
},
{
"name": "nvim_tabpage_del_var"
},
{
"name": "nvim_buf_get_name"
},
{
"name": "nvim_list_bufs"
},
{
"name": "nvim_win_set_buf"
},
{
"name": "nvim_win_close"
},
{
"name": "nvim_command_output"
},
{
"name": "nvim_command"
},
{
"name": "nvim_tabpage_get_win"
},
{
"name": "nvim_win_set_var"
},
{
"name": "nvim_buf_add_highlight"
},
{
"name": "nvim_buf_set_var"
},
{
"name": "nvim_win_get_number"
},
{
"name": "nvim_strwidth"
},
{
"name": "nvim_buf_set_lines"
},
{
"name": "nvim_err_writeln"
},
{
"name": "nvim_buf_set_option"
},
{
"name": "nvim_list_tabpages"
},
{
"name": "nvim_set_option"
},
{
"name": "nvim_buf_get_lines"
},
{
"name": "nvim_buf_get_changedtick"
},
{
"name": "nvim_win_get_tabpage"
},
{
"name": "nvim_call_function"
},
{
"name": "nvim_buf_is_valid"
}
]
}
]
]
11:54:54 DEBUG [transport] - response from vim cost: -1,114ms
11:54:54 DEBUG [connection] - received response: -2,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -2,111ms
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"set_client_info",
[
"coc",
{
"major": 0,
"minor": 0,
"patch": 79
},
"remote",
{},
{}
]
]
]
11:54:54 DEBUG [transport] - request to vim: -3,nvim_get_vvar,[
"vim_did_enter"
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"get_vvar",
[
"vim_did_enter"
]
],
-3
]
11:54:54 DEBUG [connection] - received response: -3,[
null,
1
]
11:54:54 DEBUG [transport] - response from vim cost: -3,0ms
11:54:54 DEBUG [transport] - request to vim: -4,nvim_eval,[
"&runtimepath"
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"&runtimepath"
]
],
-4
]
11:54:54 DEBUG [transport] - request to vim: -5,nvim_eval,[
"&runtimepath"
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"&runtimepath"
]
],
-5
]
11:54:54 DEBUG [connection] - received response: -4,[
null,
"/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after"
]
11:54:54 DEBUG [transport] - response from vim cost: -4,0ms
11:54:54 DEBUG [transport] - response of client cost: 1,14ms
11:54:54 DEBUG [connection] - send to vim: [
1,
[
null,
[
{
"id": "coc-eslint",
"isLocal": false,
"version": "1.3.2",
"description": "eslint extension for coc",
"exotic": false,
"uri": "",
"root": "/home/channi16/.config/coc/extensions/node_modules/coc-eslint",
"state": "unknown",
"packageJSON": {
"name": "coc-eslint",
"version": "1.3.2",
"description": "eslint extension for coc",
"main": "lib/index.js",
"publisher": "chemzqm",
"keywords": [
"coc.nvim",
"eslint"
],
"engines": {
"coc": "^0.0.64"
},
"scripts": {
"clean": "rimraf lib",
"build": "webpack",
"prepare": "npx npm-run-all clean build"
},
"activationEvents": [
"*"
],
"contributes": {
"commands": [
{
"title": "Fix all auto-fixable problems",
"category": "ESLint",
"command": "eslint.executeAutofix"
},
{
"title": "Create a '.eslintrc' config file",
"category": "ESLint",
"command": "eslint.createConfig"
}
],
"configuration": {
"type": "object",
"title": "Eslint",
"properties": {
"eslint.enable": {
"type": "boolean",
"default": true,
"description": "enable lint for files."
},
"eslint.quiet": {
"type": "boolean",
"default": false,
"description": "Turns on quiet mode, which ignores warnings."
},
"eslint.nodeEnv": {
"type": [
"string",
"null"
],
"default": null,
"description": "The value of NODE_ENV to use when running eslint tasks."
},
"eslint.trace.server": {
"type": "string",
"default": "off",
"enum": [
"off",
"messages",
"verbose"
]
},
"eslint.execArgv": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"eslint.filetypes": {
"type": "array",
"default": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"items": {
"type": "string"
}
},
"eslint.packageManager": {
"type": "string",
"default": "npm",
"enum": [
"npm",
"yarn"
]
},
"eslint.run": {
"type": "string",
"default": "onType",
"enum": [
"onType",
"onSave"
]
},
"eslint.runtime": {
"type": [
"string",
"null"
],
"default": null,
"description": "The location of the node binary to run ESLint under."
},
"eslint.nodePath": {
"type": [
"string",
"null"
],
"default": null,
"description": "A path added to NODE_PATH when resolving the eslint module."
},
"eslint.autoFix": {
"type": "boolean",
"default": true,
"description": "Enable auto fix feature"
},
"eslint.options": {
"scope": "resource",
"type": "object",
"default": {},
"description": "The eslint options object to provide args normally passed to eslint when executed from a command line (see http://eslint.org/docs/developer-guide/nodejs-api#cliengine)."
},
"eslint.autoFixOnSave": {
"type": "boolean",
"default": false
},
"eslint.autoFixSkipRules": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Rules that should be skipped when autofixing."
},
"eslint.codeAction.disableRuleComment": {
"scope": "resource",
"type": "object",
"default": {
"enable": true,
"location": "separateLine"
},
"properties": {
"enable": {
"type": "boolean",
"default": true,
"description": "Show the disable code actions."
},
"location": {
"type": "string",
"enum": [
"separateLine",
"sameLine"
],
"default": "separateLine",
"description": "Configure the disable rule code action to insert the comment on the same line or a new line."
}
}
},
"eslint.codeAction.showDocumentation": {
"scope": "resource",
"type": "object",
"default": {
"enable": true
},
"properties": {
"enable": {
"type": "boolean",
"default": true,
"description": "Show the documentation code actions."
}
}
}
}
}
},
"author": "chemzqm@gmail.com",
"license": "MIT",
"devDependencies": {
"@chemzqm/tsconfig": "^0.0.3",
"@types/eslint": "^7.2.2",
"@types/fast-diff": "^1.2.0",
"@types/node": "^10.12.0",
"coc.nvim": "0.0.79",
"eslint": "^7.9.0",
"fast-diff": "^1.2.0",
"rimraf": "^3.0.0",
"ts-loader": "^6.2.1",
"typescript": "^3.7.4",
"vscode-languageserver": "^6.1.1",
"vscode-languageserver-protocol": "3.15.3",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-uri": "^2.1.1",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"which": "^2.0.2"
},
"dependencies": {}
}
},
{
"id": "coc-json",
"isLocal": false,
"version": "1.2.6",
"description": "Json extension for coc.nvim",
"exotic": false,
"uri": "",
"root": "/home/channi16/.config/coc/extensions/node_modules/coc-json",
"state": "unknown",
"packageJSON": {
"name": "coc-json",
"version": "1.2.6",
"description": "Json extension for coc.nvim",
"main": "lib/index.js",
"publisher": "chemzqm",
"keywords": [
"coc.nvim",
"json"
],
"engines": {
"coc": ">= 0.0.70"
},
"scripts": {
"clean": "rimraf lib",
"watch": "webpack --watch",
"build": "webpack",
"prepare": "npx npm-run-all clean build"
},
"activationEvents": [
"onLanguage:json",
"onLanguage:jsonc"
],
"contributes": {
"configuration": {
"type": "object",
"title": "Json",
"properties": {
"json.enable": {
"type": "boolean",
"default": true,
"description": "Enable json server"
},
"json.trace.server": {
"type": "string",
"default": "off",
"enum": [
"off",
"messages",
"verbose"
]
},
"json.execArgv": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"json.format.enable": {
"type": "boolean",
"default": true,
"description": "Enable format for json server"
},
"json.schemas": {
"type": "array",
"scope": "resource",
"description": "Schemas associations for json files",
"default": [],
"items": {
"type": "object",
"default": {
"fileMatch": [
"/my-file"
],
"url": "schemaURL"
},
"properties": {
"url": {
"type": "string",
"default": "/user.schema.json"
},
"fileMatch": {
"type": "array",
"items": {
"type": "string",
"default": "MyFile.json"
},
"minItems": 1,
"description": "File pattern to match."
},
"schema": {
"$ref": "http://json-schema.org/draft-04/schema#",
"description": "Url of json schema, support file/url protocol."
}
}
}
}
}
}
},
"author": "chemzqm@gmail.com",
"license": "MIT",
"devDependencies": {
"@chemzqm/tsconfig": "^0.0.3",
"@chemzqm/tslint-config": "^1.0.18",
"@types/node": "^11.13.10",
"coc.nvim": "^0.0.70",
"request-light": "^0.2.4",
"rimraf": "^2.6.3",
"ts-loader": "^6.0.3",
"tslint": "^5.16.0",
"typescript": "^3",
"vscode-json-languageservice": "3.3.0",
"vscode-languageserver": "5.3.0-next.7",
"vscode-uri": "2.0.1",
"webpack": "^4.34.0",
"webpack-cli": "^3.3.4"
},
"dependencies": {}
}
}
]
]
]
11:54:54 DEBUG [connection] - received response: -5,[
null,
"/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after"
]
11:54:54 DEBUG [transport] - response from vim cost: -5,2ms
11:54:54 DEBUG [transport] - request to vim: -6,nvim_call_function,[
"coc#util#vim_info",
[]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#vim_info",
[]
]
],
-6
]
11:54:54 DEBUG [connection] - received response: -6,[
null,
{
"pid": 17367,
"version": "8021767",
"background": "light",
"columns": 173,
"completeOpt": "menu,preview",
"mode": "n",
"textprop": true,
"runtimepath": "/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after",
"progpath": "/usr/bin/vim.gtk3",
"isCygwin": false,
"disabledSources": {},
"isMacvim": false,
"floating": false,
"extensionRoot": "/home/channi16/.config/coc/extensions",
"lines": 44,
"cmdheight": 1,
"config": {},
"isVim": true,
"workspaceFolders": null,
"pumevent": 1,
"isiTerm": 0,
"filetypeMap": {},
"globalExtensions": [],
"locationlist": 1,
"colorscheme": "solarized8",
"vimCommands": [],
"watchExtensions": [],
"guicursor": "n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175"
}
]
11:54:54 DEBUG [transport] - response from vim cost: -6,25ms
11:54:54 DEBUG [transport] - request to vim: -7,nvim_list_bufs,[]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"list_bufs",
[]
],
-7
]
11:54:54 DEBUG [connection] - received response: -7,[
null,
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14
]
]
11:54:54 DEBUG [transport] - response from vim cost: -7,1ms
11:54:54 DEBUG [transport] - request to vim: -8,nvim_call_function,[
"bufnr",
[
"%"
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"bufnr",
[
"%"
]
]
],
-8
]
11:54:54 DEBUG [connection] - received response: -8,[
null,
1
]
11:54:54 DEBUG [transport] - response from vim cost: -8,0ms
11:54:54 DEBUG [transport] - request to vim: -9,nvim_call_function,[
"coc#util#get_bufoptions",
[
1
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
1
]
]
],
-9
]
11:54:54 DEBUG [transport] - request to vim: -10,nvim_call_function,[
"coc#util#get_bufoptions",
[
2
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
2
]
]
],
-10
]
11:54:54 DEBUG [transport] - request to vim: -11,nvim_call_function,[
"coc#util#get_bufoptions",
[
3
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
3
]
]
],
-11
]
11:54:54 DEBUG [transport] - request to vim: -12,nvim_call_function,[
"coc#util#get_bufoptions",
[
4
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
4
]
]
],
-12
]
11:54:54 DEBUG [transport] - request to vim: -13,nvim_call_function,[
"coc#util#get_bufoptions",
[
5
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
5
]
]
],
-13
]
11:54:54 DEBUG [transport] - request to vim: -14,nvim_call_function,[
"coc#util#get_bufoptions",
[
6
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
6
]
]
],
-14
]
11:54:54 DEBUG [transport] - request to vim: -15,nvim_call_function,[
"coc#util#get_bufoptions",
[
7
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
7
]
]
],
-15
]
11:54:54 DEBUG [transport] - request to vim: -16,nvim_call_function,[
"coc#util#get_bufoptions",
[
8
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
8
]
]
],
-16
]
11:54:54 DEBUG [transport] - request to vim: -17,nvim_call_function,[
"coc#util#get_bufoptions",
[
9
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
9
]
]
],
-17
]
11:54:54 DEBUG [transport] - request to vim: -18,nvim_call_function,[
"coc#util#get_bufoptions",
[
10
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
10
]
]
],
-18
]
11:54:54 DEBUG [transport] - request to vim: -19,nvim_call_function,[
"coc#util#get_bufoptions",
[
11
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
11
]
]
],
-19
]
11:54:54 DEBUG [transport] - request to vim: -20,nvim_call_function,[
"coc#util#get_bufoptions",
[
12
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
12
]
]
],
-20
]
11:54:54 DEBUG [transport] - request to vim: -21,nvim_call_function,[
"coc#util#get_bufoptions",
[
13
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
13
]
]
],
-21
]
11:54:54 DEBUG [transport] - request to vim: -22,nvim_call_function,[
"coc#util#get_bufoptions",
[
14
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_bufoptions",
[
14
]
]
],
-22
]
11:54:54 DEBUG [connection] - received response: -9,[
null,
{
"changedtick": 4,
"variables": {},
"winid": 1000,
"eol": 1,
"previewwindow": false,
"bufname": "innovatrix/images/api/service/services/CampaignService.js",
"fullpath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js",
"filetype": "javascript.jsx",
"buftype": "",
"iskeyword": "@,48-57,_,192-255,$",
"size": 51559
}
]
11:54:54 DEBUG [transport] - response from vim cost: -9,1ms
11:54:54 DEBUG [connection] - received response: -10,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -10,1ms
11:54:54 DEBUG [connection] - received response: -11,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -11,1ms
11:54:54 DEBUG [connection] - received response: -12,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -12,1ms
11:54:54 DEBUG [connection] - received response: -13,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -13,1ms
11:54:54 DEBUG [connection] - received response: -14,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -14,1ms
11:54:54 DEBUG [connection] - received response: -15,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -15,1ms
11:54:54 DEBUG [connection] - received response: -16,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -16,1ms
11:54:54 DEBUG [connection] - received response: -17,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -17,1ms
11:54:54 DEBUG [connection] - received response: -18,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -18,1ms
11:54:54 DEBUG [connection] - received response: -19,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -19,1ms
11:54:54 DEBUG [connection] - received response: -20,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -20,0ms
11:54:54 DEBUG [transport] - request to vim: -23,nvim_call_function,[
"getbufline",
[
1,
1,
"$"
]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"getbufline",
[
1,
1,
"$"
]
]
],
-23
]
11:54:54 DEBUG [connection] - received response: -21,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -21,1ms
11:54:54 DEBUG [connection] - received response: -22,[
null,
null
]
11:54:54 DEBUG [transport] - response from vim cost: -22,1ms
11:54:54 DEBUG [connection] - received response: -23,[
null,
[
"const log4js = require('log4js');",
"const { config } = require('cargo-service');",
"const { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');",
"const moment = require('moment-timezone');",
"const {",
" addCampaignManagerRoleToUser,",
" addUserToDxAuth,",
" getAllPossibleReviewers,",
" getPossibleCampaignManagers,",
" getScopedRightsForUser,",
" getUserFromDxAuth,",
" getUserFromDxAuthByDxAuthId,",
"} = require('./_duxisAuthService');",
"const DuxisAuthClient = require('./DuxisAuthClient');",
"const {",
" DOCUMENT_FIELD,",
" PROJECT_RECORD_FIELD,",
" DRAFT_INTAKE,",
" OPEN_INTAKE,",
" SUBMITTED_INTAKE,",
" BRUSSELS_TZ,",
"} = require('../constants/intakes');",
"",
"const log = log4js.getLogger('CampaignService');",
"",
"const SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);",
"const PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);",
"",
"function transformCampaign({",
" assessment_flows_group_id,",
" created_by,",
" created_on,",
" decision_date,",
" end_date,",
" has_evaluative_phases,",
" intake_end_date,",
" intake_start_date,",
" intake_type,",
" pitch_end,",
" pitch_start,",
" start_date,",
" updated_by,",
" updated_on,",
" ...rest",
"}) {",
" return {",
" assessmentFlowsGroupId: assessment_flows_group_id,",
" createdBy: created_by,",
" createdOn: created_on,",
" decisionDate: decision_date,",
" endDate: end_date,",
" hasEvaluativePhases: has_evaluative_phases,",
" intakeEndDate: intake_end_date,",
" intakeStartDate: intake_start_date,",
" intakeType: intake_type,",
" pitchEnd: pitch_end,",
" pitchStart: pitch_start,",
" startDate: start_date,",
" updatedBy: updated_by,",
" updatedOn: updated_on,",
" ...rest,",
" };",
"}",
"",
"class CampaignService {",
" constructor(store) {",
" this.store = store;",
" this.tableName = 'campaigns';",
" this.duxisAuthClient = new DuxisAuthClient();",
" }",
"",
" async createCampaigns(data, trx) {",
" try {",
" log.info('Writing campaigns...');",
"",
" const preSelectionPhase = await trx('phases')",
" .first('id')",
" .where('title', 'Pre-selection');",
" const selectionPhase = await trx('phases')",
" .first('id')",
" .where('title', 'Selection');",
" const projectPhase = await trx('phases')",
" .first('id')",
" .where('title', 'Project');",
"",
" let preSelId = uuid();",
" let selId = uuid();",
" let projectPhaseId = uuid();",
"",
" if (preSelectionPhase) {",
" preSelId = preSelectionPhase.id;",
" } else {",
" await trx('phases').insert({",
" id: preSelId,",
" title: 'Pre-selection',",
" });",
" }",
"",
" if (selectionPhase) {",
" selId = selectionPhase.id;",
" } else {",
" await trx('phases').insert({",
" id: selId,",
" title: 'Selection',",
" });",
" }",
"",
" if (projectPhase) {",
" projectPhaseId = projectPhase.id;",
" } else {",
" await trx('phases').insert({",
" id: projectPhaseId,",
" title: 'Project',",
" });",
" }",
"",
" await asyncForIn(data, async ({ phases, ...campaign }) => {",
" await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE",
" SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,",
" decision_date = ?, pitch_start = ?, pitch_end = ?;",
" `, [",
" campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,",
" campaign.decision_date, campaign.pitch_start, campaign.pitch_end,",
" ]);",
"",
" await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {",
" await trx.raw(`${trx('campaigns_phases').insert({",
" assessment_version_group_id: assessmentVersionGroupId,",
" deadline,",
" left_id: campaign.id,",
" right_id: title === 'Pre-selection' ? preSelId : selId,",
" order,",
" }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE",
" SET deadline = ?;",
" `, [deadline]);",
" });",
"",
" await trx.raw(`${trx('campaigns_phases')",
" .insert({",
" left_id: campaign.id,",
" right_id: projectPhaseId,",
" order: phases.length,",
" })",
" .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;",
" `, []);",
" });",
" log.info('Writing campaigns is done!');",
" } catch (e) {",
" log.warn(`createCampaigns ${e}`);",
" }",
" }",
"",
" async removeAll(trx) {",
" try {",
" await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);",
" await trx('campaigns_phases').del();",
" await trx('phases').del();",
" await trx(this.tableName).del();",
" await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);",
" } catch (e) {",
" log.warn(`removeAll: ${e}`);",
" throw e;",
" }",
" }",
"",
" async createCampaign({",
" name, phases, intakeType, intakeStartDate,",
" intakeEndDate, hasEvaluativePhases, intakeFormId,",
" }, duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" const profile = await trx('user_profiles')",
" .first('id').where('duxis_auth_id', duxisId);",
"",
" if (!profile) {",
" throw new Error('Unauthorized: user profile does not exist.');",
" }",
"",
" const campaignId = uuid();",
" const campaign = await trx('campaigns').insert({",
" id: campaignId,",
" created_by: profile.id,",
" created_on: new Date(),",
" updated_by: profile.id,",
" updated_on: new Date(),",
" name,",
" intake_type: intakeType,",
" intake_start_date: intakeStartDate,",
" intake_end_date: intakeEndDate,",
" has_evaluative_phases: hasEvaluativePhases,",
" });",
"",
" let phaseOrder = 0;",
" if (hasEvaluativePhases && phases && phases.length > 0) {",
" for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {",
" await trx('campaigns_phases').insert({",
" assessment_version_group_id: assessmentVersionGroupId,",
" deadline,",
" left_id: campaignId,",
" right_id: phaseId,",
" order: phaseOrder,",
" });",
" phaseOrder += 1;",
" }",
" }",
"",
" const projectPhase = await trx('phases')",
" .first('id')",
" .where('title', 'Project');",
"",
" if (projectPhase) {",
" await trx('campaigns_phases').insert({",
" left_id: campaignId,",
" right_id: projectPhase.id,",
" order: phaseOrder,",
" });",
" }",
"",
" // Link intake form with campaign if the intakeType is NOT creation",
" if (intakeType === 'APPLICATION' && intakeFormId) {",
" await trx('campaigns_intakes').insert({",
" left_id: campaignId,",
" right_id: intakeFormId,",
" });",
" }",
"",
" return { ...campaign, intakeFormId };",
" });",
" } catch (e) {",
" log.warn(`createCampaign: ${e}`);",
" throw e;",
" }",
" }",
"",
" async updateCampaign({",
" assessmentFlowsGroupId,",
" attendees,",
" campaignId,",
" hasEvaluativePhases,",
" intakeEndDate,",
" intakeFormId,",
" intakeStartDate,",
" intakeType,",
" name,",
" phases,",
" }, duxisId) {",
" try {",
" let campaign;",
" await this.store.knex.transaction(async (trx) => {",
" const profile = await trx('user_profiles')",
" .first('id').where('duxis_auth_id', duxisId);",
"",
" if (!profile) {",
" throw new Error('Unauthorized: user does not exist.');",
" }",
"",
" await trx('campaigns').update({",
" assessment_flows_group_id: assessmentFlowsGroupId,",
" attendees,",
" name,",
" intake_type: intakeType,",
" intake_start_date: intakeStartDate,",
" intake_end_date: intakeEndDate,",
" has_evaluative_phases: hasEvaluativePhases,",
" updated_by: profile.id,",
" updated_on: new Date(),",
" }).where('id', campaignId);",
"",
" // If no phases were explicitly provided we ignore it.",
" if (phases !== undefined) {",
" // Remove campaign phase links.",
" await trx('campaigns_phases').del().where('left_id', campaignId);",
"",
" let phaseOrder = 0;",
" if (hasEvaluativePhases && phases && phases.length > 0) {",
" for (const { deadline, phaseId, assessmentVersionId } of phases) {",
" const assessmentVersion = await trx('assessment_versions')",
" .first('id', 'group_id AS groupId')",
" .where('id', assessmentVersionId);",
" await trx('campaigns_phases').insert({",
" assessment_version_id: assessmentVersion.id,",
" assessment_version_group_id: assessmentVersion.groupId,",
" deadline,",
" left_id: campaignId,",
" right_id: phaseId,",
" order: phaseOrder,",
" });",
" phaseOrder += 1;",
" }",
" }",
"",
" const projectPhase = await trx('phases')",
" .first('id')",
" .where('title', 'Project');",
"",
" if (projectPhase) {",
" await trx('campaigns_phases').insert({",
" left_id: campaignId,",
" right_id: projectPhase.id,",
" order: phaseOrder,",
" });",
" }",
" }",
"",
" if (intakeFormId || intakeType === 'CREATION') {",
" await trx('campaigns_intakes')",
" .del()",
" .where('left_id', campaignId);",
" }",
"",
" if (intakeType === 'APPLICATION' && intakeFormId) {",
" await trx('campaigns_intakes').insert({",
" left_id: campaignId,",
" right_id: intakeFormId,",
" });",
" }",
"",
" campaign = await this.getCampaign(campaignId, trx);",
" });",
" return campaign;",
" } catch (e) {",
" log.warn(`updateCampaign: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getCampaign(campaignId, existingTrx) {",
" try {",
" let campaign;",
" const now = moment().tz(BRUSSELS_TZ);",
" await this.store.withTransaction(existingTrx, async (trx) => {",
" campaign = await trx(this.tableName)",
" .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')",
" .where('id', campaignId)",
" .first();",
"",
" campaign.phases = await trx('phases')",
" .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')",
" .join('campaigns_phases', 'right_id', 'phases.id')",
" .where('left_id', campaignId)",
" .orderBy('campaigns_phases.order');",
"",
" const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;",
" campaign.isInFirstPhase = campaignFirstPhaseDeadline",
" ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)",
" : true;",
" campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);",
" const linkedIntake = await trx('campaigns_intakes')",
" .first('right_id AS id').where('left_id', campaignId);",
" campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;",
" });",
" return transformCampaign(campaign);",
" } catch (e) {",
" log.warn(`getCampaign: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getPublicCampaign({ campaignName }) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" // Make sure the campaign exists",
" const publicCampaignData = await trx('campaigns')",
" .first('id', 'name', 'intake_end_date AS deadline')",
" .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well",
"",
" // If campaign wasn't found let's pass this INVALID_ID so we can do some error",
" // handling in the front-end",
" if (!publicCampaignData) {",
" return { id: 'INVALID_ID', callClosed: true };",
" }",
"",
" const now = moment().tz(BRUSSELS_TZ);",
" const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));",
" return { ...publicCampaignData, callClosed };",
" });",
" } catch (e) {",
" log.warn(`getPublicCampaign: ${e}`);",
" throw e;",
" }",
" }",
"",
" async registerCampaignUser({ campaignId, user }) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" // Check intake_end for current campaign",
" const campaign = await trx('campaigns')",
" .first('intake_end_date AS deadline')",
" .where('id', campaignId);",
" if (!campaign) {",
" throw new Error('Call does not exist.');",
" }",
" const now = moment().tz(BRUSSELS_TZ);",
" const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;",
" if (now.isSameOrAfter(deadline)) {",
" throw new Error('Call is closed.');",
" }",
" // Try to register the user to auth0 and in our database",
" const dxResponse = await this.duxisAuthClient.getDuxisToken();",
" const { dxToken: strippedToken } = dxResponse;",
"",
" const sanitizedUsername = user.email.toLowerCase();",
"",
" const response = await addUserToDxAuth({",
" data: { firstName: user.firstName, lastName: user.lastName },",
" enabled: true,",
" provider: 'auth0',",
" roles: ['innovatrix/role/applicant'],",
" scoped_rights: [],",
" username: sanitizedUsername,",
" password: user.password,",
" }, strippedToken, true);",
"",
" // Check if we have auth0 or auth-store errors",
" if (response && response.errors && response.errors.length > 0) {",
" if (response.errors.find((e) => e.message.includes('user already exists'))) {",
" log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);",
" return {",
" id: 'USER_ALREADY_EXISTS',",
" };",
" }",
"",
" log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));",
" return {",
" id: 'UNKNOWN_ERROR',",
" };",
" }",
"",
" // Fetch the newly added user entry from our auth-store",
" const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(",
" sanitizedUsername,",
" strippedToken",
" );",
"",
" if (!newlyRegisteredUser) {",
" log.warn('registerCampaignUser: Did not find the newly registered user.');",
" return 'UNKNOWN_ERROR';",
" }",
"",
" const id = uuid();",
" const profile = {",
" duxis_auth_id: newlyRegisteredUser.id,",
" email: sanitizedUsername,",
" first_name: user.firstName,",
" has_logged_in: false,",
" id,",
" job_title: user.role,",
" last_name: user.lastName,",
" phone_number: user.phone,",
" salutation: user.salutation,",
" linked_in_profile: user.linkedIn,",
" };",
"",
" await trx('user_profiles')",
" .insert(profile);",
"",
" return { id };",
" });",
" } catch (e) {",
" log.warn(`registerCampaignUser: ${e}`);",
" throw e;",
" }",
" }",
"",
" async addOtherCampaignData(campaigns, trx) {",
" await asyncForIn(campaigns, async (campaign) => {",
" // Get all evaluative phases for this campaign.",
" campaign.phases = await trx('phases')",
" .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')",
" .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')",
" .where('left_id', campaign.id)",
" .orderBy('campaigns_phases.order');",
"",
" if (campaign.created_by) {",
" // Get and assign creator",
" const userProfile = await trx('user_profiles')",
" .first('first_name as firstName', 'last_name as lastName')",
" .where('id', campaign.created_by);",
" campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;",
" }",
"",
" if (campaign.updated_by) {",
" // Get and assign updater.",
" const userProfile = await trx('user_profiles')",
" .first('first_name as firstName', 'last_name as lastName')",
" .where('id', campaign.updated_by);",
" campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;",
" }",
"",
" // Get our most advanced phase for this campaign.",
" let campaignPhase = await trx('campaigns_projects')",
" .first('campaigns_phases.right_id AS phaseId')",
" .join('projects', 'projects.id', 'campaigns_projects.right_id')",
" .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')",
" .orderBy('campaigns_phases.order', 'DESC')",
" .where('campaigns_projects.left_id', campaign.id);",
"",
" // If there are no projects yet, we fallback on the first phase of the campaign.",
" if (!campaignPhase) {",
" campaignPhase = await trx('campaigns_phases')",
" .first('right_id AS phaseId')",
" .orderBy('order', 'DESC')",
" .where('left_id', campaign.id);",
" }",
"",
" if (PHASES_ENABLED) {",
" const projectsInLastPhase = await trx('projects')",
" .count('projects.id')",
" .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')",
" .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')",
" .where('projects.phase_id', function whereLastPhase() {",
" this.first('right_id AS id')",
" .from('campaigns_phases')",
" .whereRaw('left_id = campaigns_projects.left_id')",
" .orderBy('order', 'DESC');",
" })",
" .where('campaigns.id', campaign.id)",
" .first();",
" campaign.projectsInLastPhase = Number(projectsInLastPhase.count);",
" }",
"",
" const projectsInCampaign = await trx('campaigns_projects')",
" .select('projects.id AS id', 'projects.name AS name')",
" .join('projects', 'projects.id', 'campaigns_projects.right_id')",
" .where('left_id', campaign.id);",
"",
" campaign.projects = projectsInCampaign;",
"",
" campaign.numberOfProjects = (projectsInCampaign || []).length;",
"",
" campaign.phaseId = campaignPhase && campaignPhase.phaseId;",
"",
" const linkedIntake = await trx('campaigns_intakes')",
" .first('right_id AS id').where('left_id', campaign.id);",
" campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;",
" });",
" }",
"",
" async getCampaigns() {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" const campaigns = await this.store.getCollection(",
" this.tableName,",
" { ordering: [{ field: 'start_date', direction: 'desc' }], trx }",
" );",
" await this.addOtherCampaignData(campaigns, trx);",
" return campaigns.map(transformCampaign);",
" });",
" } catch (e) {",
" log.warn(`getCampaigns: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getScopedCampaigns(duxisId) {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user');",
" }",
" // Fetch all campaigns linked to the current user",
" const assignedCampaignIds = await trx('campaigns_managers')",
" .select('left_id as id')",
" .where('right_id', profile.id);",
"",
" // Fetch their data",
" const campaigns = await trx('campaigns')",
" .select('*')",
" .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));",
"",
" // Add additional data...",
" await this.addOtherCampaignData(campaigns, trx);",
"",
" // Transform the data and pass it to front-end",
" return campaigns.map(transformCampaign);",
" });",
" } catch (e) {",
" log.warn(`getScopedCampaigns: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getCampaignManagers({ campaignId }, duxisId) {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
"",
" // Get all the user_profile ids that are assigned for the current campaign",
" const managerIdsForCurrentCampaign = await trx('campaigns_managers')",
" .select('right_id AS id')",
" .where('left_id', campaignId);",
"",
" if (managerIdsForCurrentCampaign.length === 0) {",
" return [];",
" }",
"",
" const campaignManagers = await trx('user_profiles')",
" .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')",
" .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));",
"",
" return campaignManagers.map((manager) => ({",
" ...manager,",
" name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,",
" }));",
" });",
" } catch (e) {",
" log.warn(`getCampaignManagers ${e}`);",
" throw e;",
" }",
" }",
"",
" // { campaignId: { projects: [ projectId, projectId, ... ]} }",
" async addProjects(campaignId, projectIds, trx) {",
" try {",
" await asyncForIn(projectIds.projects, async (projectId) => {",
" const writeObject = { left_id: campaignId, right_id: projectId };",
" await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);",
" });",
" } catch (e) {",
" log.warn(`addProjects ${e}`);",
" throw e;",
" }",
" }",
"",
" async removeAllRelations(trx) {",
" try {",
" await trx('campaigns_projects').del();",
" } catch (e) {",
" log.warn(`removeAll: ${e}`);",
" throw e;",
" }",
" }",
"",
" async createPhase({ title }) {",
" try {",
" const phase = { id: uuid(), title };",
" await this.store.knex('phases').insert(phase);",
" return phase;",
" } catch (e) {",
" log.warn(`createPhase: ${e}`);",
" throw e;",
" }",
" }",
"",
" // Used for dropdown with all available phases.",
" async getPhases() {",
" try {",
" return await this.store.knex('phases')",
" .select('id', 'title')",
" .orderBy('title');",
" } catch (e) {",
" log.warn(`getPhases: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getAllPossibleReviewers(duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" // Check if the user exists",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
"",
" // Get a duxis token to make calls to the auth container",
" const dxResponse = await this.duxisAuthClient.getDuxisToken();",
" const { dxToken: strippedToken } = dxResponse;",
"",
" const response = await getAllPossibleReviewers(strippedToken);",
" const possibleReviewers = (response && response.items) || [];",
" const result = [];",
" await asyncForIn(possibleReviewers, async (reviewer) => {",
" const matchingProfile = await trx('user_profiles')",
" .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')",
" .where('duxis_auth_id', reviewer.id)",
" .andWhere('email', reviewer.username);",
" // Only push to the result array if we have a matching user_profile entry",
" if (matchingProfile) {",
" result.push({",
" ...matchingProfile,",
" ...reviewer,",
" name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,",
" profileId: matchingProfile.profileId,",
" });",
" }",
" });",
" const alphabeticallySortedReviewersList = result",
" // eslint-disable-next-line",
" .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));",
" return alphabeticallySortedReviewersList;",
" });",
" } catch (err) {",
" log.warn(`getAllPossibleReviewers: ${err}`);",
" throw new Error(err);",
" }",
" }",
"",
" // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them",
" async createCampaignReviewer({ user }, duxisId) {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" // Check if the user exists",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
"",
" // Try to register the user to auth0 and in our database",
" const dxResponse = await this.duxisAuthClient.getDuxisToken();",
" const { dxToken: strippedToken } = dxResponse;",
"",
" const sanitizedUsername = user.email.toLowerCase();",
"",
" const response = await addUserToDxAuth({",
" data: { firstName: user.firstName, lastName: user.lastName },",
" enabled: true,",
" provider: 'auth0',",
" roles: ['innovatrix/role/evaluator'],",
" scoped_rights: [],",
" username: sanitizedUsername,",
" }, strippedToken, false);",
"",
" // Check if we have auth0 or auth-store errors",
" if (response && response.errors && response.errors.length > 0) {",
" if (response.errors.find((e) => e.message.includes('user already exists'))) {",
" log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);",
" return {",
" id: 'USER_ALREADY_EXISTS',",
" };",
" }",
"",
" log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));",
" return {",
" id: 'UNKNOWN_ERROR',",
" };",
" }",
"",
" // Fetch the newly added user entry from our auth-store",
" const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(",
" sanitizedUsername,",
" strippedToken",
" );",
"",
" if (!newlyRegisteredUser) {",
" log.warn('createCampaignReviewer: Did not find the newly registered user.');",
" return 'UNKNOWN_ERROR';",
" }",
"",
" const id = uuid();",
" const newProfile = {",
" duxis_auth_id: newlyRegisteredUser.id,",
" email: sanitizedUsername,",
" first_name: user.firstName,",
" has_logged_in: false,",
" id,",
" last_name: user.lastName,",
" };",
"",
" await trx('user_profiles')",
" .insert(newProfile);",
"",
" return { id };",
" });",
" } catch (err) {",
" log.warn(`createCampaignReviewer: ${err}`);",
" throw new Error(err);",
" }",
" }",
"",
" async persistCampaignReviewers({ campaignId, reviewerIds }) {",
" if (!campaignId) {",
" throw new Error('campaignId is required.');",
" }",
" if (!reviewerIds || !Array.isArray(reviewerIds)) {",
" throw new Error('reviewerIds is required.');",
" }",
"",
" await this.store.knex.transaction(async (trx) => {",
" const deletedCampaignReviewers = await trx('campaigns_reviewers')",
" .select('right_id AS userProfileId')",
" .where('left_id', campaignId)",
" .whereNotIn('right_id', reviewerIds);",
"",
" // Insert new campaign reviewers",
" await asyncForIn(reviewerIds, async (id) => {",
" const exists = await trx('campaigns_reviewers')",
" .first('right_id')",
" .where('right_id', id)",
" .andWhere('left_id', campaignId);",
" if (!exists) {",
" await trx('campaigns_reviewers')",
" .insert({",
" left_id: campaignId,",
" right_id: id,",
" });",
" }",
" });",
"",
" if (deletedCampaignReviewers.length === 0) { return; }",
"",
" const currentCampaignProjects = await trx('campaigns_projects')",
" .select('right_id AS projectId')",
" .where('left_id', campaignId);",
"",
" const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);",
" const projectIds = currentCampaignProjects.map((c) => c.projectId);",
"",
" await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {",
" // Delete all project linked to the deleted reviewer(s)",
" await trx('projects_reviewers')",
" .del()",
" .whereIn('left_id', projectIds)",
" .andWhere('right_id', deletedReviewerId);",
"",
" // Delete campaign reviewer entry",
" await trx('campaigns_reviewers')",
" .del()",
" .where('right_id', deletedReviewerId)",
" .andWhere('left_id', campaignId);",
" });",
" });",
" }",
"",
" async getCampaignReviewers(campaignId) {",
" try {",
" return await this.store.knex('campaigns_reviewers')",
" .select('right_id AS id')",
" .where('left_id', campaignId);",
" } catch (e) {",
" log.warn(`getCampaignReviewers: ${e}`);",
" throw e;",
" }",
" }",
"",
" async getCampaignReviewerGroups(duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" // Check if the user exists",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
"",
" // Get all groups and their reviewers",
" const reviewerGroups = await trx('reviewer_groups')",
" .select('id', 'title', 'collapsed')",
" .orderBy('title');",
"",
" const result = [];",
" await asyncForIn(reviewerGroups, async (reviewerGroup) => {",
" const reviewerIds = await trx('reviewer_group_reviewers')",
" .select('right_id AS id')",
" .where('left_id', reviewerGroup.id);",
" result.push({",
" ...reviewerGroup,",
" reviewerIds: reviewerIds.map((i) => i.id),",
" });",
" });",
" return result;",
" });",
" } catch (e) {",
" log.warn(`getCampaignReviewerGroups: ${e}`);",
" throw e;",
" }",
" }",
"",
" async persistCampaignReviewerGroups({ groups }, duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" // Check if the user exists",
" const profile = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
"",
" // Get all the currently stored groups' ids",
" const storedReviewerGroupIds = await trx('reviewer_groups')",
" .select('id');",
" // Check which groups we need to delete",
" const deletedGroupIds = storedReviewerGroupIds",
" .filter((r) => !groups.map((g) => g.id).includes(r.id))",
" .map((g) => g.id);",
" // Create/update groups",
" await asyncForIn(groups, async (group) => {",
" // Check if reviewer_group already exists...",
" const exists = await trx('reviewer_groups')",
" .first('id')",
" .where('id', group.id);",
" if (exists) {",
" // update if it does",
" await trx('reviewer_groups')",
" .update({",
" title: group.title,",
" collapsed: group.collapsed,",
" })",
" .where('id', group.id);",
" // First delete all reviewers, then we re-insert the current ones",
" // This way we don't need to check which ones to delete or update",
" await trx('reviewer_group_reviewers')",
" .del()",
" .where('left_id', group.id);",
" } else {",
" // create if it doesn't",
" await trx('reviewer_groups')",
" .insert({",
" id: group.id,",
" title: group.title,",
" collapsed: group.collapsed,",
" });",
" }",
" // Insert the reviewers",
" const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values",
" await asyncForIn(reviewerIds, async (reviewerId) => {",
" await trx('reviewer_group_reviewers')",
" .insert({",
" left_id: group.id,",
" right_id: reviewerId,",
" });",
" });",
" });",
"",
" // Finally remove deleted groups",
" await asyncForIn(deletedGroupIds, async (deletedGroupId) => {",
" await trx('reviewer_group_reviewers')",
" .del()",
" .where('left_id', deletedGroupId);",
" await trx('reviewer_groups')",
" .del()",
" .where('id', deletedGroupId);",
" });",
" });",
" } catch (e) {",
" log.warn(`createCampaignReviewerGroups: ${e}`);",
" throw e;",
" }",
" }",
"",
" async deleteCampaign(campaignId) {",
" try {",
" await this.store.knex.transaction(async (trx) => {",
" await trx('campaigns_phases')",
" .del()",
" .where('left_id', campaignId);",
" log.info('Deleted campaigns_phases WHERE left_id', campaignId);",
" await trx('campaigns_projects')",
" .del()",
" .where('left_id', campaignId);",
" log.info('Deleted campaigns_projects WHERE left_id', campaignId);",
" await trx('campaigns_reviewers')",
" .del()",
" .where('left_id', campaignId);",
" log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);",
" // Delete linked intakes",
" await trx('campaigns_intakes')",
" .del()",
" .where('left_id', campaignId);",
" await trx('campaigns')",
" .del()",
" .where('id', campaignId);",
" log.info('Deleted campaign WHERE id', campaignId);",
" });",
" } catch (e) {",
" log.warn(`deleteCampaign: ${e}`);",
" throw e;",
" }",
" }",
"",
" async makeCampaignsExport() {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" const phases = await trx('phases')",
" .select('id', 'title');",
" const projects = await trx('projects')",
" .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')",
" .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')",
" .where('deleted_on', null);",
" const projectDecisions = await trx('project_phase_statuses')",
" .select('status', 'project_id AS projectId', 'phase_id AS phaseId');",
"",
" const campaigns = await trx('campaigns')",
" .select('id', 'name');",
"",
" for (const campaign of campaigns) {",
" campaign.phases = (await trx('campaigns_phases')",
" .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')",
" .orderBy('order'));",
" }",
"",
" let evaluations;",
"",
" if (SALESFORCE_ENABLED) {",
" evaluations = await trx('evaluations')",
" .select(",
" 'evaluations.id', 'project_id AS projectId',",
" 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'",
" )",
" .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')",
" .where('submitted', true)",
" .orderBy('order');",
"",
" for (const evaluation of evaluations) {",
" // Only get ratings for quantified questions.",
" evaluation.ratings = await trx('evaluation_ratings')",
" .select(",
" 'evaluation_ratings.id', 'score', 'question_id AS questionId',",
" 'evaluation_ratings.domain_id AS domainId',",
" 'evaluation_ratings.criterium_id AS criteriumId',",
" 'criteria.pre_selection_threshold AS preSelectionThreshold',",
" 'criteria.selection_threshold AS selectionThreshold'",
" )",
" .join('questions', 'questions.id', 'evaluation_ratings.question_id')",
" .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')",
" .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');",
" }",
"",
" for (const project of projects) {",
" project.reviews = await trx('reviews')",
" .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')",
" .where('project_id', project.id);",
" }",
" } else {",
" evaluations = await trx('evaluations')",
" .select(",
" 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',",
" 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'",
" )",
" .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')",
" .where('completed', true)",
" .whereNot('reviewer_id', null)",
" .orderBy('order');",
"",
" for (const evaluation of evaluations) {",
" // Only get ratings for quantified questions.",
" evaluation.ratings = await trx('evaluation_ratings')",
" .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')",
" .join('questions', 'questions.id', 'evaluation_ratings.question_id')",
" .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');",
"",
" const review = await trx('new_reviews')",
" .first(",
" 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',",
" 'advice_type AS advice',",
" )",
" .join('question_options', 'question_options.id', 'new_reviews.question_option_id')",
" .join('questions', 'questions.id', 'question_options.question_id')",
" .where('new_reviews.evaluation_id', evaluation.id);",
"",
" evaluation.advice = review && review.advice;",
" }",
" }",
"",
" return {",
" campaigns,",
" evaluations,",
" phases,",
" projects,",
" projectDecisions,",
" };",
" });",
" } catch (e) {",
" throw new Error('makeCampaignsExport failed');",
" }",
" }",
"",
" async _checkRoles(userId, roleToCheck) {",
" const dxResponse = await this.duxisAuthClient.getDuxisToken();",
" const { dxToken: strippedToken } = dxResponse;",
" const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);",
" const { data: { getUser: { user: userData } } } = dxUser;",
" if (!userData || !userData.roles) {",
" // Should we throw an error?",
" return false;",
" }",
" return (userData.roles || []).includes(roleToCheck);",
" }",
"",
" async createCampaignManager({ campaignId, user }) {",
" try {",
" await this.store.knex.transaction(async (trx) => {",
" const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();",
" let userProfileId = user.id;",
" let userDuxisAuthId = user.duxisAuthId;",
" const sanitizedUsername = (user.email || '').toLowerCase();",
" // No id provided... so we assume this is going to be a new user in our database",
" if (!userProfileId) {",
" // Email is required field",
" if (!sanitizedUsername) {",
" throw new Error('The user needs an email to be invited.');",
" }",
" // Check if the email is already being used as a username in our auth-store",
" const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);",
" if (data.getUser.user) {",
" throw new Error('User with current email already exists.');",
" }",
" // Add the new user to our database",
" // with normal client role and campaign_manager role",
" await addUserToDxAuth({",
" data: { firstName: user.firstName, lastName: user.lastName },",
" enabled: true,",
" provider: 'auth0',",
" roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],",
" scoped_rights: [],",
" username: sanitizedUsername,",
" }, strippedToken, false);",
"",
" // Fetch the newly added user entry from our auth-store",
" const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(",
" sanitizedUsername,",
" strippedToken",
" );",
"",
" if (newlyAddedUser) {",
" const id = uuid();",
" userDuxisAuthId = newlyAddedUser.id;",
" // Add new user_profile entry",
" await trx('user_profiles')",
" .insert({",
" id,",
" duxis_auth_id: userDuxisAuthId,",
" email: sanitizedUsername,",
" first_name: user.firstName,",
" last_name: user.lastName,",
" has_logged_in: false,",
" });",
"",
" // Check if has been successfully created",
" const userEntry = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', userDuxisAuthId);",
"",
" // We set the newly created user_profile's id to link with the campaign",
" if (userEntry) {",
" userProfileId = userEntry.id;",
" } else {",
" throw new Error('Something went wrong while creating the new user.');",
" }",
" } else {",
" throw new Error('Something went wrong while getting the user from the auth database');",
" }",
" } else {",
" // The user has an id, so he should exist in our database",
" const profile = await trx('user_profiles')",
" .first('id', 'email')",
" .where('duxis_auth_id', userDuxisAuthId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
" // Add \"campaign manager\" role to the user if he does NOT have the role yet",
" // else we skip this step",
" const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');",
" if (!hasCampaignManagerRole) {",
" let roles;",
" let scopedRights;",
" const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);",
" const { data: { getUser: { user: userData } } } = dxUser;",
" if (!userData || !userData.roles) {",
" // Should we throw an error?",
" roles = [];",
" scopedRights = [];",
" } else {",
" roles = (userData.roles || []);",
" const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);",
" scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);",
" }",
" await addCampaignManagerRoleToUser({",
" data: userData.data,",
" provider: userData.provider,",
" roles,",
" scopedRights,",
" userId: userDuxisAuthId,",
" username: profile.email, // username in auth-store is the same as email in api-store",
" enabled: userData.enabled,",
" }, strippedToken);",
" }",
" }",
"",
" // Finally we link the user profile to the campaign...",
" await trx('campaigns_managers')",
" .insert({",
" right_id: userProfileId,",
" left_id: campaignId,",
" });",
" });",
" } catch (e) {",
" log.warn(`createCampaignManager: ${e}`);",
" throw new Error(e);",
" }",
" }",
"",
" async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {",
" try {",
" await this.store.knex.transaction(async (trx) => {",
" const profile = await trx('user_profiles')",
" .select('id')",
" .where('duxis_auth_id', duxisId);",
" if (!profile) {",
" throw new Error('Unauthorized user.');",
" }",
" await trx('campaigns_managers')",
" .del()",
" .where('right_id', userProfileId)",
" .andWhere('left_id', campaignId);",
" });",
" } catch (e) {",
" log.warn(`deleteCampaignManager: ${e}`);",
" throw new Error(e);",
" }",
" }",
"",
" async getPossibleCampaignManagers(searchString) {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" const dxResponse = await this.duxisAuthClient.getDuxisToken();",
" const { dxToken: strippedToken } = dxResponse;",
" const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);",
" const result = [];",
" await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {",
" const user = await trx('user_profiles')",
" .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')",
" .where('duxis_auth_id', possibleUser.id);",
" if (user) {",
" result.push(user);",
" }",
" });",
" return result;",
" });",
" } catch (e) {",
" log.warn(`getPossibleCampaignManagers: ${e}`);",
" throw e;",
" }",
" }",
"",
" _getIntakeCallStatus(id, intakes) {",
" const hasIntake = intakes.find((i) => i.intakeId === id);",
" if (hasIntake) {",
" return {",
" status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,",
" submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,",
" };",
" }",
" return {",
" status: OPEN_INTAKE,",
" submittedOn: null,",
" };",
" }",
"",
" async getCalls(duxisId) {",
" try {",
" return this.store.knex.transaction(async (trx) => {",
" const user = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
"",
" if (!user) {",
" throw new Error('Unauthorized: user does not exist.');",
" }",
"",
" const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');",
" const allCampaigns = await trx('campaigns')",
" .select(",
" 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',",
" 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'",
" )",
" .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')",
" .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')",
" .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!",
"",
" // fetch the current user's submitted/draft intakes, if any",
" // we should fetch an array of objects like:",
" // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],",
" const intakesCurrentUser = await trx('intake_answers')",
" .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')",
" .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')",
" .where('user_profile_id', user.id);",
"",
" const calls = {};",
" // This should only be filtered on the pitchEnd",
" // but let's add a fallback (for now) just in case no pitchEnd was set",
" // for open calls we have 3 states,",
" // 1) OPEN 2) DRAFT 3) SUBMITTED",
" calls.openCalls = allCampaigns",
" .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them",
" .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))",
" .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))",
" // Sort by soonest first",
" .sort((x, y) => x.intakeEndDate - y.intakeEndDate);",
" // for closed calls we only have 2 states,",
" // 1) DRAFT 2) SUBMITTED",
" calls.closedCalls = allCampaigns",
" .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))",
" .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))",
" // Sort by most recently passed first",
" .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status",
" .sort((x, y) => y.intakeEndDate - x.intakeEndDate);",
"",
" return calls;",
" });",
" } catch (e) {",
" log.warn(`getCalls: ${e}`);",
" throw e;",
" }",
" }",
"",
" // Get a list of all calls",
" // ( = campaigns with an IntakeForm linked to them)",
" async manageCalls(duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" const user = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
"",
" if (!user) {",
" throw new Error('Unauthorized: user does not exist.');",
" }",
"",
" const calls = await trx('campaigns_intakes')",
" .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')",
" .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');",
"",
" return calls;",
" });",
" } catch (e) {",
" log.warn(`manageCalls: ${e}`);",
" throw e;",
" }",
" }",
"",
" async callOverview({ callId }, duxisId) {",
" try {",
" return await this.store.knex.transaction(async (trx) => {",
" const user = await trx('user_profiles')",
" .first('id')",
" .where('duxis_auth_id', duxisId);",
"",
" if (!user) {",
" throw new Error('Unauthorized: user does not exist.');",
" }",
"",
" const call = await trx('campaigns_intakes')",
" .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')",
" .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')",
" .where('campaigns.id', callId);",
"",
" if (!call) {",
" throw new Error('Call does not exist.');",
" }",
"",
" // Get all necessary data for the frontend",
" let result = {",
" ...call,",
" };",
"",
" // Get the answer data",
" const storedApplications = await trx('intake_answers')",
" .select(",
" 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',",
" 'user_profiles.email', 'user_profiles.first_name AS firstName',",
" 'user_profiles.last_name AS lastName', 'created_on AS created_on',",
" )",
" .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')",
" .where('campaign_id', callId)",
" .andWhere('intake_id', call.intakeId)",
" // sort by submitted_on first, drafts will be grouped together",
" .orderBy('submitted_on', 'desc')",
" // sort drafts by created_on",
" .orderBy('intake_answers.created_on', 'desc');",
"",
" const applications = [];",
" await asyncForIn(storedApplications, async ({ answers, ...rest }) => {",
" // parse the JSON",
" const parsedAnswers = JSON.parse(answers);",
" // find the provided PROJECT_RECORD_FIELD if it exists",
" const projectNameField = parsedAnswers",
" .map((section) => section.items)",
" .flat()",
" .find((field) => field.type === PROJECT_RECORD_FIELD);",
" // get the project name (if field exists)",
" const projectName = (projectNameField && projectNameField.value) || 'No project name provided';",
" // structure the object for the frontend",
" const structuredObject = {",
" ...rest,",
" projectName,",
" };",
" // add it to the list of applications",
" applications.push(structuredObject);",
" });",
"",
" result = {",
" ...result,",
" applications,",
" };",
"",
" const documents = [];",
" await asyncForIn(storedApplications, async ({ answers, ...rest }) => {",
" // parse the JSON",
" const parsedAnswers = JSON.parse(answers);",
" const intakeDocuments = await trx('intake_documents')",
" .select('intake_question_id AS questionId')",
" .where('intake_answer_id', rest.id);",
" const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);",
" // find the document fields with uploaded documents",
" const documentFields = parsedAnswers",
" .map((section) => section.items)",
" .flat()",
" .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));",
" // Only push to the results array if there effectively are",
" // uploaded documents available on a question",
" if (documentFields.length > 0) {",
" documents.push({",
" ...rest,",
" uploadedDocuments: documentFields,",
" });",
" }",
" });",
"",
" result = {",
" ...result,",
" documents,",
" };",
"",
" return result;",
" });",
" } catch (e) {",
" log.warn(`callOverview: ${e}`);",
" throw e;",
" }",
" }",
"}",
"",
"module.exports = CampaignService;"
]
]
11:54:54 DEBUG [transport] - response from vim cost: -23,3ms
11:54:54 DEBUG [transport] - request to vim: -24,nvim_call_function,[
"win_getid",
[]
]
11:54:54 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"win_getid",
[]
]
],
-24
]
11:54:54 DEBUG [connection] - received response: -24,[
null,
1000
]
11:54:54 DEBUG [transport] - response from vim cost: -24,4ms
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#_watch",
[
"coc_sources_disable_map"
]
]
]
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_atomic",
[
[
[
"nvim_command",
[
"sign define CocError linehl=CocErrorLine texthl=CocErrorSign text=>>"
]
],
[
"nvim_command",
[
"sign define CocWarning linehl=CocWarningLine texthl=CocWarningSign text=⚠"
]
],
[
"nvim_command",
[
"sign define CocInfo linehl=CocInfoLine texthl=CocInfoSign text=>>"
]
],
[
"nvim_command",
[
"sign define CocHint linehl=CocHintLine texthl=CocHintSign text=>>"
]
]
]
]
]
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"command",
[
"sign define CocSelected text=* texthl=CocSelectedText linehl=CocSelectedLine"
]
]
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"set_var",
[
"coc_workspace_initialized",
1
]
]
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"set_var",
[
"WorkspaceFolders",
[
"/home/channi16/imec/innovatrix"
]
]
]
]
11:54:55 DEBUG [transport] - request to vim: -25,nvim_command,[
"source /tmp/vFnmXCJ/coc.nvim-17368/coc-17368.vim"
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"command",
[
"source /tmp/vFnmXCJ/coc.nvim-17368/coc-17368.vim"
]
],
-25
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"set_var",
[
"coc_service_initialized",
1
]
]
]
11:54:55 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#util#do_autocmd",
[
"CocNvimInit"
]
]
]
]
11:54:55 DEBUG [connection] - received response: -25,[
null,
0
]
11:54:55 DEBUG [transport] - response from vim cost: -25,12ms
11:54:56 DEBUG [transport] - request to vim: -26,nvim_eval,[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
11:54:56 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
],
-26
]
11:54:56 DEBUG [connection] - received response: -26,[
null,
[
1,
"n",
1,
531,
{
"title": ""
}
]
]
11:54:56 DEBUG [transport] - response from vim cost: -26,1ms
11:54:56 DEBUG [transport] - request to vim: -27,nvim_call_atomic,[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 0,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
0,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
11:54:56 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_atomic",
[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 0,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
0,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
],
-27
]
11:54:56 DEBUG [connection] - received response: -27,[
null,
[
[
0,
0,
0
],
null
]
]
11:54:56 DEBUG [transport] - response from vim cost: -27,1ms
11:54:56 DEBUG [transport] - request to vim: -28,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:54:56 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-28
]
11:54:56 DEBUG [connection] - received response: -28,[
null,
[
1,
[
530,
0
],
"javascript.jsx",
"n",
0,
0
]
]
11:54:56 DEBUG [transport] - response from vim cost: -28,0ms
11:54:56 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertEnter",
1
]
]
11:54:56 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#util#clear_highlights",
[]
]
]
]
11:54:56 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"command",
[
"redraw"
]
]
]
11:54:56 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
531,
65
]
]
]
11:54:57 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
531,
64
]
]
]
11:54:57 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 531,
"col": 64,
"changedtick": 5,
"pre": " campaign.phaseId = campaignPhase && campaignPhase.phaseId"
}
]
]
11:54:57 DEBUG [transport] - request to vim: -29,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:54:57 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-29
]
11:54:57 DEBUG [connection] - received response: -29,[
null,
{
"changedtick": 5,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:54:57 DEBUG [transport] - response from vim cost: -29,7ms
11:54:57 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertLeave",
1
]
]
11:54:57 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
531,
63
]
]
]
11:54:57 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
532,
1
]
]
]
11:54:58 DEBUG [transport] - request to vim: -30,nvim_eval,[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
11:54:58 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
],
-30
]
11:54:58 DEBUG [connection] - received response: -30,[
null,
[
1,
"n",
1,
532,
{
"title": ""
}
]
]
11:54:58 DEBUG [transport] - response from vim cost: -30,1ms
11:54:58 DEBUG [transport] - request to vim: -31,nvim_call_atomic,[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 1,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
531,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_command",
[
"sign place 1000 line=531 name=CocError buffer=1"
]
],
[
"nvim_buf_add_highlight",
[
1,
1082,
"CocErrorHighlight",
530,
63,
63
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
11:54:58 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_atomic",
[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 1,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
531,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_command",
[
"sign place 1000 line=531 name=CocError buffer=1"
]
],
[
"nvim_buf_add_highlight",
[
1,
1082,
"CocErrorHighlight",
530,
63,
63
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
],
-31
]
11:54:58 DEBUG [connection] - received response: -31,[
null,
[
[
0,
0,
0,
0,
0
],
null
]
]
11:54:58 DEBUG [transport] - response from vim cost: -31,2ms
11:54:58 DEBUG [transport] - request to vim: -32,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:54:58 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-32
]
11:54:58 DEBUG [connection] - received response: -32,[
null,
[
1,
[
531,
0
],
"javascript.jsx",
"n",
0,
0
]
]
11:54:58 DEBUG [transport] - response from vim cost: -32,37ms
11:54:58 DEBUG [transport] - request to vim: -33,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:54:58 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-33
]
11:54:58 DEBUG [connection] - received response: -33,[
null,
{
"changedtick": 5,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:54:58 DEBUG [transport] - response from vim cost: -33,8ms
11:54:59 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
531,
63
]
]
]
11:54:59 DEBUG [transport] - request to vim: -34,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:54:59 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-34
]
11:54:59 DEBUG [connection] - received response: -34,[
null,
[
1,
[
530,
62
],
"javascript.jsx",
"n",
0,
0
]
]
11:54:59 DEBUG [transport] - response from vim cost: -34,1ms
11:55:01 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
531,
64
]
]
]
11:55:01 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChanged",
1,
7
]
]
11:55:01 DEBUG [transport] - request to vim: -35,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-35
]
11:55:01 DEBUG [connection] - received response: -35,[
null,
{
"changedtick": 7,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:01 DEBUG [transport] - response from vim cost: -35,7ms
11:55:01 DEBUG [transport] - request to vim: -36,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-36
]
11:55:01 DEBUG [connection] - received response: -36,[
null,
[
1,
[
530,
63
],
"javascript.jsx",
"n",
0,
0
]
]
11:55:01 DEBUG [transport] - response from vim cost: -36,0ms
11:55:01 DEBUG [transport] - request to vim: -37,nvim_call_function,[
"coc#float#get_float_mode",
[
false,
false,
false
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#float#get_float_mode",
[
false,
false,
false
]
]
],
-37
]
11:55:01 DEBUG [connection] - received response: -37,[
null,
[
"n",
1,
[
22,
65
],
[
531,
64
],
{
"columns": 173,
"lines": 44,
"cmdheight": 1
}
]
]
11:55:01 DEBUG [transport] - response from vim cost: -37,1ms
11:55:01 DEBUG [transport] - request to vim: -38,nvim_call_function,[
"coc#float#create_float_win",
[
0,
0,
{
"height": 1,
"width": 45,
"row": 1,
"col": 0,
"relative": "cursor"
}
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#float#create_float_win",
[
0,
0,
{
"height": 1,
"width": 45,
"row": 1,
"col": 0,
"relative": "cursor"
}
]
]
],
-38
]
11:55:01 DEBUG [connection] - received response: -38,[
null,
[
1002,
15,
0
]
]
11:55:01 DEBUG [transport] - response from vim cost: -38,2ms
11:55:01 DEBUG [transport] - request to vim: -39,nvim_call_atomic,[
[
[
"nvim_call_function",
[
"clearmatches",
[
1002
]
]
],
[
"nvim_call_function",
[
"coc#util#set_buf_lines",
[
15,
[
"[eslint semi] [E] Missing semicolon. (semi)"
]
]
]
],
[
"nvim_buf_add_highlight",
[
15,
1086,
"CocErrorFloat",
0,
0,
43
]
],
[
"nvim_call_function",
[
"win_execute",
[
1002,
"noa normal! gg0"
]
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_atomic",
[
[
[
"nvim_call_function",
[
"clearmatches",
[
1002
]
]
],
[
"nvim_call_function",
[
"coc#util#set_buf_lines",
[
15,
[
"[eslint semi] [E] Missing semicolon. (semi)"
]
]
]
],
[
"nvim_buf_add_highlight",
[
15,
1086,
"CocErrorFloat",
0,
0,
43
]
],
[
"nvim_call_function",
[
"win_execute",
[
1002,
"noa normal! gg0"
]
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
],
-39
]
11:55:01 DEBUG [connection] - received response: -39,[
null,
[
[
0,
0,
0,
"",
0
],
null
]
]
11:55:01 DEBUG [transport] - response from vim cost: -39,1ms
11:55:01 DEBUG [transport] - request to vim: -40,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-40
]
11:55:01 DEBUG [connection] - received response: -40,[
null,
{
"changedtick": 7,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:01 DEBUG [transport] - response from vim cost: -40,9ms
11:55:01 DEBUG [transport] - request to vim: -41,nvim_eval,[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]"
]
],
-41
]
11:55:01 DEBUG [connection] - received response: -41,[
null,
[
1,
"n",
1,
531,
{
"title": ""
}
]
]
11:55:01 DEBUG [transport] - response from vim cost: -41,1ms
11:55:01 DEBUG [transport] - request to vim: -42,nvim_call_atomic,[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 0,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
0,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_call_function",
[
"coc#util#unplace_signs",
[
1,
[
1000
]
]
]
],
[
"nvim_buf_clear_namespace",
[
1,
1082,
0,
-1
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_atomic",
[
[
[
"nvim_call_function",
[
"coc#util#set_buf_var",
[
1,
"coc_diagnostic_info",
{
"error": 0,
"warning": 0,
"information": 0,
"hint": 0,
"lnums": [
0,
0,
0,
0
]
}
]
]
],
[
"nvim_call_function",
[
"coc#util#do_autocmd",
[
"CocDiagnosticChange"
]
]
],
[
"nvim_call_function",
[
"coc#util#unplace_signs",
[
1,
[
1000
]
]
]
],
[
"nvim_buf_clear_namespace",
[
1,
1082,
0,
-1
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
],
-42
]
11:55:01 DEBUG [connection] - received response: -42,[
null,
[
[
0,
0,
0,
0,
0
],
null
]
]
11:55:01 DEBUG [transport] - response from vim cost: -42,2ms
11:55:01 DEBUG [transport] - request to vim: -43,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:55:01 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-43
]
11:55:02 DEBUG [connection] - received response: -43,[
null,
[
1,
[
530,
63
],
"javascript.jsx",
"n",
0,
0
]
]
11:55:02 DEBUG [transport] - response from vim cost: -43,53ms
11:55:02 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_atomic",
[
[
[
"nvim_call_function",
[
"coc#float#close",
[
1002
]
]
],
[
"nvim_call_function",
[
"coc#float#close",
[
0
]
]
],
[
"nvim_command",
[
"redraw"
]
]
]
]
]
]
11:55:02 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"BufWinLeave",
15,
-1
]
]
11:55:02 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#util#clear_pos_matches",
[
"^Coc",
-1
]
]
]
]
11:55:02 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"BufHidden",
15
]
]
11:55:03 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
532,
1
]
]
]
11:55:03 DEBUG [transport] - request to vim: -44,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:55:03 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-44
]
11:55:03 DEBUG [connection] - received response: -44,[
null,
[
1,
[
531,
0
],
"javascript.jsx",
"n",
0,
0
]
]
11:55:03 DEBUG [transport] - response from vim cost: -44,2ms
11:55:06 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMoved",
1,
[
531,
64
]
]
]
11:55:06 DEBUG [transport] - request to vim: -45,nvim_eval,[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
11:55:06 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]"
]
],
-45
]
11:55:06 DEBUG [connection] - received response: -45,[
null,
[
1,
[
530,
63
],
"javascript.jsx",
"n",
0,
0
]
]
11:55:06 DEBUG [transport] - response from vim cost: -45,1ms
11:55:06 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertEnter",
1
]
]
11:55:06 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#util#clear_highlights",
[]
]
]
]
11:55:06 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"command",
[
"redraw"
]
]
]
11:55:06 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
532,
7
]
]
]
11:55:06 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 532,
"col": 7,
"changedtick": 8,
"pre": " "
}
]
]
11:55:07 DEBUG [transport] - request to vim: -46,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:07 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-46
]
11:55:07 DEBUG [connection] - received response: -46,[
null,
{
"changedtick": 8,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:07 DEBUG [transport] - response from vim cost: -46,11ms
11:55:07 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
7
]
]
]
11:55:07 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 7,
"changedtick": 9,
"pre": " "
}
]
]
11:55:07 DEBUG [transport] - request to vim: -47,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:07 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-47
]
11:55:07 DEBUG [connection] - received response: -47,[
null,
{
"changedtick": 9,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:07 DEBUG [transport] - response from vim cost: -47,8ms
11:55:07 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"c"
]
]
11:55:07 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
8
]
]
]
11:55:07 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 8,
"changedtick": 10,
"pre": " c"
}
]
]
11:55:07 DEBUG [transport] - request to vim: -48,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:07 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-48
]
11:55:07 DEBUG [transport] - request to vim: -49,nvim_eval,[
"[coc#util#cursor(), getline(\".\")]"
]
11:55:07 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#cursor(), getline(\".\")]"
]
],
-49
]
11:55:08 DEBUG [connection] - received response: -48,[
null,
{
"changedtick": 10,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n c\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -48,3ms
11:55:08 DEBUG [transport] - request to vim: -50,nvim_call_function,[
"coc#util#get_complete_option",
[]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_complete_option",
[]
]
],
-50
]
11:55:08 DEBUG [connection] - received response: -49,[
null,
[
[
532,
7
],
" c"
]
]
11:55:08 DEBUG [transport] - response from vim cost: -49,16ms
11:55:08 DEBUG [connection] - received response: -50,[
null,
{
"word": "c",
"bufnr": 1,
"col": 6,
"synname": "jsFuncBlock",
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js",
"blacklist": [],
"line": " c",
"filetype": "javascript.jsx",
"linenr": 533,
"input": "c",
"colnr": 8,
"changedtick": 10
}
]
11:55:08 DEBUG [transport] - response from vim cost: -50,1ms
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"o"
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
9
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 9,
"changedtick": 11,
"pre": " co"
}
]
]
11:55:08 DEBUG [transport] - request to vim: -51,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-51
]
11:55:08 DEBUG [transport] - request to vim: -52,nvim_eval,[
"[coc#util#cursor(), getline(\".\")]"
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#cursor(), getline(\".\")]"
]
],
-52
]
11:55:08 DEBUG [connection] - received response: -51,[
null,
{
"changedtick": 11,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n co\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -51,4ms
11:55:08 DEBUG [transport] - request to vim: -53,nvim_call_function,[
"coc#util#get_complete_option",
[]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_complete_option",
[]
]
],
-53
]
11:55:08 DEBUG [connection] - received response: -52,[
null,
[
[
532,
8
],
" co"
]
]
11:55:08 DEBUG [transport] - response from vim cost: -52,8ms
11:55:08 DEBUG [connection] - received response: -53,[
null,
{
"word": "co",
"bufnr": 1,
"col": 6,
"synname": "jsFuncBlock",
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js",
"blacklist": [],
"line": " co",
"filetype": "javascript.jsx",
"linenr": 533,
"input": "co",
"colnr": 9,
"changedtick": 11
}
]
11:55:08 DEBUG [transport] - response from vim cost: -53,0ms
11:55:08 DEBUG [transport] - request to vim: -54,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-54
]
11:55:08 DEBUG [connection] - received response: -54,[
null,
{
"changedtick": 11,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n co\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -54,3ms
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"n"
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
10
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 10,
"changedtick": 12,
"pre": " con"
}
]
]
11:55:08 DEBUG [transport] - request to vim: -55,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-55
]
11:55:08 DEBUG [transport] - request to vim: -56,nvim_eval,[
"[coc#util#cursor(), getline(\".\")]"
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#cursor(), getline(\".\")]"
]
],
-56
]
11:55:08 DEBUG [connection] - received response: -55,[
null,
{
"changedtick": 12,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -55,3ms
11:55:08 DEBUG [transport] - request to vim: -57,nvim_call_function,[
"coc#util#get_complete_option",
[]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_complete_option",
[]
]
],
-57
]
11:55:08 DEBUG [transport] - request to vim: -58,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-58
]
11:55:08 DEBUG [connection] - received response: -56,[
null,
[
[
532,
9
],
" con"
]
]
11:55:08 DEBUG [transport] - response from vim cost: -56,7ms
11:55:08 DEBUG [connection] - received response: -57,[
null,
{
"word": "con",
"bufnr": 1,
"col": 6,
"synname": "jsFuncBlock",
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js",
"blacklist": [],
"line": " con",
"filetype": "javascript.jsx",
"linenr": 533,
"input": "con",
"colnr": 10,
"changedtick": 12
}
]
11:55:08 DEBUG [transport] - response from vim cost: -57,0ms
11:55:08 DEBUG [connection] - received response: -58,[
null,
{
"changedtick": 12,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -58,2ms
11:55:08 DEBUG [transport] - request to vim: -59,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-59
]
11:55:08 DEBUG [connection] - received response: -59,[
null,
{
"changedtick": 12,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -59,4ms
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"command",
[
"noa set completeopt=noselect,menuone"
]
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#_do_complete",
[
6,
[
{
"word": "const",
"equal": 1,
"abbr": "const",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}"
},
{
"word": "config",
"equal": 1,
"abbr": "config",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":1}"
},
{
"word": "constants",
"equal": 1,
"abbr": "constants",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}"
},
{
"word": "container",
"equal": 1,
"abbr": "container",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":33}"
},
{
"word": "created_on",
"equal": 1,
"abbr": "created_on",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":6}"
},
{
"word": "constructor",
"equal": 1,
"abbr": "constructor",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}"
},
{
"word": "createdOn",
"equal": 1,
"abbr": "createdOn",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":8}"
},
{
"word": "count",
"equal": 1,
"abbr": "count",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":28}"
},
{
"word": "creation",
"equal": 1,
"abbr": "creation",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":17}"
}
],
-1
]
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"MenuPopupChanged",
{
"col": 6,
"row": 25,
"scrollbar": false,
"completed_item": {},
"width": 16,
"height": 9,
"size": 9
},
24
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedP",
1,
{
"lnum": 533,
"col": 10,
"changedtick": 12,
"pre": " con"
}
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"s"
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"MenuPopupChanged",
{
"col": 6,
"row": 25,
"scrollbar": false,
"completed_item": {},
"width": 16,
"height": 9,
"size": 9
},
24
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedP",
1,
{
"lnum": 533,
"col": 11,
"changedtick": 21,
"pre": " cons"
}
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#_do_complete",
[
6,
[
{
"word": "const",
"equal": 1,
"abbr": "const",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}"
},
{
"word": "constants",
"equal": 1,
"abbr": "constants",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}"
},
{
"word": "constructor",
"equal": 1,
"abbr": "constructor",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}"
}
],
-1
]
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CompleteDone",
{}
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"MenuPopupChanged",
{
"col": 6,
"row": 25,
"scrollbar": false,
"completed_item": {},
"width": 16,
"height": 3,
"size": 3
},
24
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"t"
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"MenuPopupChanged",
{
"col": 6,
"row": 25,
"scrollbar": false,
"completed_item": {},
"width": 16,
"height": 3,
"size": 3
},
24
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedP",
1,
{
"lnum": 533,
"col": 12,
"changedtick": 32,
"pre": " const"
}
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_function",
[
"coc#_do_complete",
[
6,
[
{
"word": "const",
"equal": 1,
"abbr": "const",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}"
},
{
"word": "constants",
"equal": 1,
"abbr": "constants",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}"
},
{
"word": "constructor",
"equal": 1,
"abbr": "constructor",
"menu": "[A]",
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}"
}
],
-1
]
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CompleteDone",
{}
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"MenuPopupChanged",
{
"col": 6,
"row": 25,
"scrollbar": false,
"completed_item": {},
"width": 16,
"height": 3,
"size": 3
},
24
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CompleteDone",
{}
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
" "
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
13
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 13,
"changedtick": 33,
"pre": " const "
}
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#notify",
[
"call_atomic",
[
[
[
"nvim_command",
[
"noa set completeopt=menu,preview"
]
],
[
"nvim_command",
[
"let g:coc#_context['candidates'] = []"
]
],
[
"nvim_call_function",
[
"coc#_hide",
[]
]
]
]
]
]
]
11:55:08 DEBUG [transport] - request to vim: -60,nvim_eval,[
"[coc#util#cursor(), getline(\".\")]"
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#cursor(), getline(\".\")]"
]
],
-60
]
11:55:08 DEBUG [connection] - received response: -60,[
null,
[
[
532,
12
],
" const "
]
]
11:55:08 DEBUG [transport] - response from vim cost: -60,3ms
11:55:08 DEBUG [transport] - request to vim: -61,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-61
]
11:55:08 DEBUG [connection] - received response: -61,[
null,
{
"changedtick": 33,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -61,7ms
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"InsertCharPre",
"t"
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"CursorMovedI",
1,
[
533,
14
]
]
]
11:55:08 DEBUG [connection] - received notification: [
"CocAutocmd",
[
"TextChangedI",
1,
{
"lnum": 533,
"col": 14,
"changedtick": 34,
"pre": " const t"
}
]
]
11:55:08 DEBUG [transport] - request to vim: -62,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-62
]
11:55:08 DEBUG [transport] - request to vim: -63,nvim_eval,[
"[coc#util#cursor(), getline(\".\")]"
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"eval",
[
"[coc#util#cursor(), getline(\".\")]"
]
],
-63
]
11:55:08 DEBUG [connection] - received response: -62,[
null,
{
"changedtick": 34,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const t\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;"
}
]
11:55:08 DEBUG [transport] - response from vim cost: -62,12ms
11:55:08 DEBUG [transport] - request to vim: -64,nvim_call_function,[
"coc#util#get_complete_option",
[]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_complete_option",
[]
]
],
-64
]
11:55:08 DEBUG [connection] - received response: -63,[
null,
[
[
532,
13
],
" const t"
]
]
11:55:08 DEBUG [transport] - response from vim cost: -63,30ms
11:55:08 DEBUG [connection] - received response: -64,[
null,
{
"word": "t",
"bufnr": 1,
"col": 12,
"synname": "jsFuncBlock",
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js",
"blacklist": [],
"line": " const t",
"filetype": "javascript.jsx",
"linenr": 533,
"input": "t",
"colnr": 14,
"changedtick": 34
}
]
11:55:08 DEBUG [transport] - response from vim cost: -64,1ms
11:55:08 DEBUG [transport] - request to vim: -65,nvim_call_function,[
"coc#util#get_content",
[
1
]
]
11:55:08 DEBUG [connection] - send to vim: [
"call",
"coc#api#call",
[
"call_function",
[
"coc#util#get_content",
[
1
]
]
],
-65
]
11:55:08 DEBUG [connection] - received response: -65,[
null,
{
"changedtick": 34,
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const t\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set th
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment