diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d78f5b7c7534..a81bdc6d4058 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -273,7 +273,7 @@ codemagic.yaml /libs/api/domains/directorate-of-labour/ @island-is/deloitte /libs/application/template-api-modules/src/lib/modules/templates/social-insurance-administration @island-is/deloitte /libs/application/templates/social-insurance-administration/ @island-is/deloitte -/libs/clients/social-insurance-administration/ @island-is/deloitte @island-is/stefna +/libs/clients/social-insurance-administration/ @island-is/deloitte @island-is/stefna @island-is/hugsmidjan /libs/application/templates/car-recycling/ @island-is/deloitte /libs/application/template-api-modules/src/lib/modules/templates/car-recycling/ @island-is/deloitte /libs/clients/car-recycling/ @island-is/deloitte diff --git a/.github/actions/change-detection.ts b/.github/actions/change-detection.ts index 5f15fea66944..80dc85092e4b 100644 --- a/.github/actions/change-detection.ts +++ b/.github/actions/change-detection.ts @@ -29,36 +29,43 @@ export async function findBestGoodRefBranch( baseBranch: string, workflowId: WorkflowID, ): Promise { - const log = app.extend('findBestGoodRefBranch') - log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`) - const commits = await getCommits(git, headBranch, baseBranch, 'HEAD~1') - const builds = await githubApi.getLastGoodBranchBuildRun( - headBranch, - workflowId, - commits, - ) - if (builds) - return { - sha: builds.head_commit, - run_number: builds.run_nr, - branch: headBranch, - ref: builds.head_commit, + return new Promise(async (resolve) => { + const log = app.extend('findBestGoodRefBranch') + log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`) + const commits = await getCommits(git, headBranch, baseBranch, 'HEAD~1') + const builds = await githubApi.getLastGoodBranchBuildRun( + headBranch, + workflowId, + commits, + ) + if (builds) { + resolve({ + sha: builds.head_commit, + run_number: builds.run_nr, + branch: headBranch, + ref: builds.head_commit, + }) + return } - const baseCommits = await githubApi.getLastGoodBranchBuildRun( - baseBranch, - workflowId, - commits, - ) - if (baseCommits) - return { - ref: baseCommits.head_commit, - sha: baseCommits.head_commit, - run_number: baseCommits.run_nr, - branch: baseBranch, + const baseCommits = await githubApi.getLastGoodBranchBuildRun( + baseBranch, + workflowId, + commits, + ) + if (baseCommits) { + resolve({ + ref: baseCommits.head_commit, + sha: baseCommits.head_commit, + run_number: baseCommits.run_nr, + branch: baseBranch, + }) + return } - return 'rebuild' + resolve('rebuild') + return + }) } /*** @@ -111,82 +118,89 @@ export async function findBestGoodRefPR( prBranch: string, workflowId: WorkflowID, ): Promise { - const log = app.extend('findBestGoodRefPR') - log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`) - const lastCommitSha = await git.lastCommit() - const prCommits = await getCommits(git, headBranch, baseBranch, 'HEAD') + return new Promise(async (resolve) => { + const log = app.extend('findBestGoodRefPR') + log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`) + const lastCommitSha = await git.lastCommit() + const prCommits = await getCommits(git, headBranch, baseBranch, 'HEAD') - const prRun = await githubApi.getLastGoodPRRun( - headBranch, - workflowId, - prCommits, - ) - const prBuilds: { - distance: number - hash: string - run_nr: number - branch: string - ref: string - }[] = [] - if (prRun) { - log(`Found a PR run candidate: ${JSON.stringify(prRun)}`) - try { - const tempBranch = `${headBranch}-${Math.round(Math.random() * 1000000)}` - await git.checkoutBranch(tempBranch, prRun.base_commit) - log(`Branch checked out`) - const mergeCommitSha = await git.merge(prRun.head_commit) - log(`Simulated previous PR merge commit`) - const distance = await githubApi.getChangedComponents( + const prRun = await githubApi.getLastGoodPRRun( + headBranch, + workflowId, + prCommits, + ) + const prBuilds: { + distance: number + hash: string + run_nr: number + branch: string + ref: string + }[] = [] + if (prRun) { + log(`Found a PR run candidate: ${JSON.stringify(prRun)}`) + try { + const tempBranch = `${headBranch}-${Math.round( + Math.random() * 1000000, + )}` + await git.checkoutBranch(tempBranch, prRun.base_commit) + log(`Branch checked out`) + const mergeCommitSha = await git.merge(prRun.head_commit) + log(`Simulated previous PR merge commit`) + const distance = await githubApi.getChangedComponents( + git, + lastCommitSha, + mergeCommitSha, + ) + log(`Affected components since candidate PR run are ${distance}`) + prBuilds.push({ + distance: diffWeight(distance), + hash: prRun.head_commit, + run_nr: prRun.run_nr, + branch: headBranch, + ref: mergeCommitSha, + }) + } catch (e) { + log( + `Error processing PR candidate(${prRun.run_nr}) but continuing: %O`, + e, + ) + } finally { + await git.checkout(prBranch) + } + } + + const baseCommits = await getCommits(git, prBranch, baseBranch, 'HEAD~1') + + const baseGoodBuilds = await githubApi.getLastGoodBranchBuildRun( + baseBranch, + 'push', + baseCommits, + ) + if (baseGoodBuilds) { + let affectedComponents = await githubApi.getChangedComponents( git, lastCommitSha, - mergeCommitSha, + baseGoodBuilds.head_commit, ) - log(`Affected components since candidate PR run are ${distance}`) prBuilds.push({ - distance: diffWeight(distance), - hash: prRun.head_commit, - run_nr: prRun.run_nr, - branch: headBranch, - ref: mergeCommitSha, + distance: diffWeight(affectedComponents), + hash: baseGoodBuilds.head_commit, + run_nr: baseGoodBuilds.run_nr, + branch: baseBranch, + ref: baseGoodBuilds.head_commit, }) - } catch (e) { - log( - `Error processing PR candidate(${prRun.run_nr}) but continuing: %O`, - e, - ) - } finally { - await git.checkout(prBranch) } - } - - const baseCommits = await getCommits(git, prBranch, baseBranch, 'HEAD~1') - - const baseGoodBuilds = await githubApi.getLastGoodBranchBuildRun( - baseBranch, - 'push', - baseCommits, - ) - if (baseGoodBuilds) { - let affectedComponents = await githubApi.getChangedComponents( - git, - lastCommitSha, - baseGoodBuilds.head_commit, - ) - prBuilds.push({ - distance: diffWeight(affectedComponents), - hash: baseGoodBuilds.head_commit, - run_nr: baseGoodBuilds.run_nr, - branch: baseBranch, - ref: baseGoodBuilds.head_commit, - }) - } - prBuilds.sort((a, b) => (a.distance > b.distance ? 1 : -1)) - if (prBuilds.length > 0) - return { - sha: prBuilds[0].hash, - run_number: prBuilds[0].run_nr, - branch: prBuilds[0].branch.replace('origin/', ''), - ref: prBuilds[0].ref, + prBuilds.sort((a, b) => (a.distance > b.distance ? 1 : -1)) + if (prBuilds.length > 0) { + resolve({ + sha: prBuilds[0].hash, + run_number: prBuilds[0].run_nr, + branch: prBuilds[0].branch.replace('origin/', ''), + ref: prBuilds[0].ref, + }) + return } - return 'rebuild' + resolve('rebuild') + return + }) } diff --git a/.github/actions/dist/main.js b/.github/actions/dist/main.js index fb9ddace8f2a..d4ad424ecedd 100644 --- a/.github/actions/dist/main.js +++ b/.github/actions/dist/main.js @@ -26107,34 +26107,41 @@ var import_debug2 = __toESM(require_src()); var app2 = (0, import_debug2.default)("change-detection"); function findBestGoodRefBranch(commitScore, git, githubApi, headBranch, baseBranch, workflowId) { return __async(this, null, function* () { - const log = app2.extend("findBestGoodRefBranch"); - log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`); - const commits = yield getCommits(git, headBranch, baseBranch, "HEAD~1"); - const builds = yield githubApi.getLastGoodBranchBuildRun( - headBranch, - workflowId, - commits - ); - if (builds) - return { - sha: builds.head_commit, - run_number: builds.run_nr, - branch: headBranch, - ref: builds.head_commit - }; - const baseCommits = yield githubApi.getLastGoodBranchBuildRun( - baseBranch, - workflowId, - commits - ); - if (baseCommits) - return { - ref: baseCommits.head_commit, - sha: baseCommits.head_commit, - run_number: baseCommits.run_nr, - branch: baseBranch - }; - return "rebuild"; + return new Promise((resolve) => __async(this, null, function* () { + const log = app2.extend("findBestGoodRefBranch"); + log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`); + const commits = yield getCommits(git, headBranch, baseBranch, "HEAD~1"); + const builds = yield githubApi.getLastGoodBranchBuildRun( + headBranch, + workflowId, + commits + ); + if (builds) { + resolve({ + sha: builds.head_commit, + run_number: builds.run_nr, + branch: headBranch, + ref: builds.head_commit + }); + return; + } + const baseCommits = yield githubApi.getLastGoodBranchBuildRun( + baseBranch, + workflowId, + commits + ); + if (baseCommits) { + resolve({ + ref: baseCommits.head_commit, + sha: baseCommits.head_commit, + run_number: baseCommits.run_nr, + branch: baseBranch + }); + return; + } + resolve("rebuild"); + return; + })); }); } function getCommits(git, headBranch, baseBranch, head, maxCount = 300) { @@ -26152,75 +26159,82 @@ function getCommits(git, headBranch, baseBranch, head, maxCount = 300) { } function findBestGoodRefPR(diffWeight, git, githubApi, headBranch, baseBranch, prBranch, workflowId) { return __async(this, null, function* () { - const log = app2.extend("findBestGoodRefPR"); - log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`); - const lastCommitSha = yield git.lastCommit(); - const prCommits = yield getCommits(git, headBranch, baseBranch, "HEAD"); - const prRun = yield githubApi.getLastGoodPRRun( - headBranch, - workflowId, - prCommits - ); - const prBuilds = []; - if (prRun) { - log(`Found a PR run candidate: ${JSON.stringify(prRun)}`); - try { - const tempBranch = `${headBranch}-${Math.round(Math.random() * 1e6)}`; - yield git.checkoutBranch(tempBranch, prRun.base_commit); - log(`Branch checked out`); - const mergeCommitSha = yield git.merge(prRun.head_commit); - log(`Simulated previous PR merge commit`); - const distance = yield githubApi.getChangedComponents( + return new Promise((resolve) => __async(this, null, function* () { + const log = app2.extend("findBestGoodRefPR"); + log(`Starting with head branch ${headBranch} and base branch ${baseBranch}`); + const lastCommitSha = yield git.lastCommit(); + const prCommits = yield getCommits(git, headBranch, baseBranch, "HEAD"); + const prRun = yield githubApi.getLastGoodPRRun( + headBranch, + workflowId, + prCommits + ); + const prBuilds = []; + if (prRun) { + log(`Found a PR run candidate: ${JSON.stringify(prRun)}`); + try { + const tempBranch = `${headBranch}-${Math.round( + Math.random() * 1e6 + )}`; + yield git.checkoutBranch(tempBranch, prRun.base_commit); + log(`Branch checked out`); + const mergeCommitSha = yield git.merge(prRun.head_commit); + log(`Simulated previous PR merge commit`); + const distance = yield githubApi.getChangedComponents( + git, + lastCommitSha, + mergeCommitSha + ); + log(`Affected components since candidate PR run are ${distance}`); + prBuilds.push({ + distance: diffWeight(distance), + hash: prRun.head_commit, + run_nr: prRun.run_nr, + branch: headBranch, + ref: mergeCommitSha + }); + } catch (e) { + log( + `Error processing PR candidate(${prRun.run_nr}) but continuing: %O`, + e + ); + } finally { + yield git.checkout(prBranch); + } + } + const baseCommits = yield getCommits(git, prBranch, baseBranch, "HEAD~1"); + const baseGoodBuilds = yield githubApi.getLastGoodBranchBuildRun( + baseBranch, + "push", + baseCommits + ); + if (baseGoodBuilds) { + let affectedComponents = yield githubApi.getChangedComponents( git, lastCommitSha, - mergeCommitSha + baseGoodBuilds.head_commit ); - log(`Affected components since candidate PR run are ${distance}`); prBuilds.push({ - distance: diffWeight(distance), - hash: prRun.head_commit, - run_nr: prRun.run_nr, - branch: headBranch, - ref: mergeCommitSha + distance: diffWeight(affectedComponents), + hash: baseGoodBuilds.head_commit, + run_nr: baseGoodBuilds.run_nr, + branch: baseBranch, + ref: baseGoodBuilds.head_commit }); - } catch (e) { - log( - `Error processing PR candidate(${prRun.run_nr}) but continuing: %O`, - e - ); - } finally { - yield git.checkout(prBranch); } - } - const baseCommits = yield getCommits(git, prBranch, baseBranch, "HEAD~1"); - const baseGoodBuilds = yield githubApi.getLastGoodBranchBuildRun( - baseBranch, - "push", - baseCommits - ); - if (baseGoodBuilds) { - let affectedComponents = yield githubApi.getChangedComponents( - git, - lastCommitSha, - baseGoodBuilds.head_commit - ); - prBuilds.push({ - distance: diffWeight(affectedComponents), - hash: baseGoodBuilds.head_commit, - run_nr: baseGoodBuilds.run_nr, - branch: baseBranch, - ref: baseGoodBuilds.head_commit - }); - } - prBuilds.sort((a, b) => a.distance > b.distance ? 1 : -1); - if (prBuilds.length > 0) - return { - sha: prBuilds[0].hash, - run_number: prBuilds[0].run_nr, - branch: prBuilds[0].branch.replace("origin/", ""), - ref: prBuilds[0].ref - }; - return "rebuild"; + prBuilds.sort((a, b) => a.distance > b.distance ? 1 : -1); + if (prBuilds.length > 0) { + resolve({ + sha: prBuilds[0].hash, + run_number: prBuilds[0].run_nr, + branch: prBuilds[0].branch.replace("origin/", ""), + ref: prBuilds[0].ref + }); + return; + } + resolve("rebuild"); + return; + })); }); } @@ -26320,7 +26334,7 @@ var SimpleGit = class { // main.ts var FULL_REBUILD_NEEDED = "full_rebuild_needed"; (() => __async(exports, null, function* () { - if (process.env.NX_AFFECTED_ALL === "true") { + if (process.env.NX_AFFECTED_ALL === "true" || process.env.TEST_EVERYTHING === "true") { console.log(FULL_REBUILD_NEEDED); return; } @@ -26345,11 +26359,12 @@ var FULL_REBUILD_NEEDED = "full_rebuild_needed"; ); if (rev === "rebuild") { console.log(FULL_REBUILD_NEEDED); - return; + process.exit(0); } rev.branch = rev.branch.replace(/'/g, ""); rev.ref = rev.ref.replace(/'/g, ""); console.log(JSON.stringify(rev)); + process.exit(0); }))(); /*! * is-plain-object diff --git a/.github/actions/main.ts b/.github/actions/main.ts index 3deb43ad0586..5a9c0be3abe0 100644 --- a/.github/actions/main.ts +++ b/.github/actions/main.ts @@ -7,7 +7,10 @@ import { WorkflowID } from './git-action-status' const FULL_REBUILD_NEEDED = 'full_rebuild_needed' ;(async () => { - if (process.env.NX_AFFECTED_ALL === 'true') { + if ( + process.env.NX_AFFECTED_ALL === 'true' || + process.env.TEST_EVERYTHING === 'true' + ) { console.log(FULL_REBUILD_NEEDED) return } @@ -37,9 +40,10 @@ const FULL_REBUILD_NEEDED = 'full_rebuild_needed' if (rev === 'rebuild') { console.log(FULL_REBUILD_NEEDED) - return + process.exit(0) } rev.branch = rev.branch.replace(/'/g, '') rev.ref = rev.ref.replace(/'/g, '') console.log(JSON.stringify(rev)) + process.exit(0) })() diff --git a/.github/actions/package.json b/.github/actions/package.json index 061faaf0a65e..28ff702a834d 100644 --- a/.github/actions/package.json +++ b/.github/actions/package.json @@ -19,7 +19,7 @@ "@octokit/rest": "19.0.4", "@types/debug": "4.1.7", "@types/jest": "^27.4.1", - "@types/node": "20.11.4", + "@types/node": "20.12.12", "debug": "4.3.4", "esbuild": "0.15.10", "esbuild-runner": "2.2.1", diff --git a/.github/actions/tsconfig.json b/.github/actions/tsconfig.json index ff324a0e1d98..1c3a88d4b3fb 100644 --- a/.github/actions/tsconfig.json +++ b/.github/actions/tsconfig.json @@ -12,7 +12,7 @@ "target": "es2015", "module": "esnext", "typeRoots": ["node_modules/@types"], - "lib": ["es2019", "esnext.array"], + "lib": ["es2019", "esnext.array", "esnext"], "skipLibCheck": true, "skipDefaultLibCheck": true, "allowSyntheticDefaultImports": true, diff --git a/.github/actions/unit-test/action.yml b/.github/actions/unit-test/action.yml index 2d26e5bb94e0..dcea4d884e94 100644 --- a/.github/actions/unit-test/action.yml +++ b/.github/actions/unit-test/action.yml @@ -36,9 +36,9 @@ runs: run: npm install -g yarn shell: bash - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn diff --git a/.github/actions/yarn.lock b/.github/actions/yarn.lock index 366f3c0d008f..6a7376cdfd0d 100644 --- a/.github/actions/yarn.lock +++ b/.github/actions/yarn.lock @@ -1610,12 +1610,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.11.4": - version: 20.11.4 - resolution: "@types/node@npm:20.11.4" +"@types/node@npm:20.12.12": + version: 20.12.12 + resolution: "@types/node@npm:20.12.12" dependencies: undici-types: ~5.26.4 - checksum: b9cf2c5397ea31f3355656edd204aee777a36db75b79b8b7aba2bed7ea5b29914fa808489da5c632c5eddbb33c3106188bef0bff3b7648bd39aa50dee466a73b + checksum: 5373983874b9af7c216e7ca5d26b32a8d9829c703a69f1e66f2113598b5be8582c0e009ca97369f1ec9a6282b3f92812208d06eb1e9fc3bd9b939b022303d042 languageName: node linkType: hard @@ -1715,7 +1715,7 @@ __metadata: "@octokit/rest": 19.0.4 "@types/debug": 4.1.7 "@types/jest": ^27.4.1 - "@types/node": 20.11.4 + "@types/node": 20.12.12 debug: 4.3.4 esbuild: 0.15.10 esbuild-runner: 2.2.1 diff --git a/.github/workflows/config-values.yaml b/.github/workflows/config-values.yaml index a5e5655f5e81..af314b1368dd 100644 --- a/.github/workflows/config-values.yaml +++ b/.github/workflows/config-values.yaml @@ -61,9 +61,9 @@ jobs: - uses: actions/checkout@v3 if: ${{ github.event_name != 'pull_request' }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -109,9 +109,9 @@ jobs: matrix: ${{ fromJson(needs.prepare.outputs.ENVS) }} steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Cache for NodeJS dependencies id: node-modules diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index ce3186ec3ea7..9c5dcaa47d2c 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -45,9 +45,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18.8.0' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -80,11 +80,9 @@ jobs: - name: Calculate cache key for node modules id: calculate_node_modules_hash run: | - PACKAGE_JSON_HASH=$(cat package.json | jq '{resolutions,dependencies,devDependencies}' | sha1sum -t | cut -f1 -d" ") - echo "PACKAGE_JSON_HASH: $PACKAGE_JSON_HASH" - export NODE_MODULES_HASH=${{ runner.os }}-${{ hashFiles('yarn.lock') }}-$PACKAGE_JSON_HASH - echo "NODE_MODULES_HASH: $NODE_MODULES_HASH" - echo "node-modules-hash=$NODE_MODULES_HASH" >> $GITHUB_OUTPUT + HASH="$(./scripts/ci/get-node-modules-hash.mjs)" + echo "node-modules-hash: ${HASH}" + echo "node-modules-hash=${HASH}" >> $GITHUB_OUTPUT - name: Calculate cache keys for generated files id: calculate_generated_files_cache_key @@ -119,9 +117,6 @@ jobs: source ./scripts/ci/00_prepare-base-tags.sh $(git merge-base HEAD $GITHUB_BASE_REF) git checkout $GITHUB_SHA echo "BASE=$BASE" >> $GITHUB_ENV - if [ -n "${NX_AFFECTED_ALL+x}" ]; then - echo "NX_AFFECTED_ALL=$NX_AFFECTED_ALL" >> $GITHUB_ENV - fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HTML_URL: ${{ github.event.pull_request.html_url }} @@ -249,9 +244,9 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -310,9 +305,9 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -373,9 +368,9 @@ jobs: path: node_modules key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -416,9 +411,9 @@ jobs: - uses: actions/checkout@v3 if: ${{ github.ref == 'ref/heads/main' }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -460,9 +455,9 @@ jobs: matrix: ${{ fromJson(needs.prepare.outputs.LINT_CHUNKS) }} steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn - name: Cache for NodeJS dependencies - host OS @@ -509,9 +504,9 @@ jobs: if: needs.prepare.outputs.BUILD_CHUNKS steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn - name: Cache for NodeJS dependencies - host OS diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 744f17f62839..ed33617bba7a 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -48,6 +48,7 @@ jobs: CREATE_PATTERNS: ^release/ PRE_RELEASE_PATTERN: ^pre-release/ outputs: + NODE_IMAGE_TAG: ${{ steps.git-branch.outputs.NODE_IMAGE_TAG }} GIT_BRANCH: ${{ steps.git-branch.outputs.GIT_BRANCH }} GIT_BRANCH_DEPLOY: ${{ steps.git-branch-deploy.outputs.GIT_BRANCH_DEPLOY }} FEATURE_NAME: ${{ steps.git-branch-deploy.outputs.FEATURE_NAME }} @@ -75,7 +76,6 @@ jobs: echo "GIT_BRANCH_DEPLOY=${GIT_BRANCH_DEPLOY}" >> $GITHUB_OUTPUT echo "GIT_BRANCH_DEPLOY=$GIT_BRANCH_DEPLOY" >> $GITHUB_ENV echo "FEATURE_NAME=$(echo $GIT_BRANCH_DEPLOY | cut -d"/" -f2- | tr -cd '[:alnum:]-' | tr '[:upper:]' '[:lower:]' | cut -c1-50)" >> $GITHUB_OUTPUT - - name: Check if we want to run workflow id: should-run env: @@ -158,6 +158,7 @@ jobs: outputs: TEST_CHUNKS: ${{ steps.test_projects.outputs.CHUNKS }} DOCKER_TAG: ${{ steps.docker_tags.outputs.DOCKER_TAG }} + NODE_IMAGE_TAG: ${{ steps.nodejs_image.outputs.NODE_IMAGE_TAG }} LAST_GOOD_BUILD_DOCKER_TAG: ${{ steps.git_nx_base.outputs.LAST_GOOD_BUILD_DOCKER_TAG }} UNAFFECTED: ${{ steps.unaffected.outputs.UNAFFECTED }} BUILD_CHUNKS: ${{ steps.build_map.outputs.BUILD_CHUNKS }} @@ -168,9 +169,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18.8.0' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -235,6 +236,15 @@ jobs: path: event.json retention-days: 60 + - name: Generate nodejs image tag + id: nodejs_image + continue-on-error: false + run: | + export NODE_IMAGE_TAG="$(./scripts/ci/get-node-version.mjs)" + echo "NODE_IMAGE_TAG: ${NODE_IMAGE_TAG}" + echo "NODE_IMAGE_TAG=${NODE_IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "NODE_IMAGE_TAG=${NODE_IMAGE_TAG}" >> $GITHUB_ENV + echo "**NODE_IMAGE_TAG** ${NODE_IMAGE_TAG}" >> $GITHUB_STEP_SUMMARY - name: Generate docker image tag id: docker_tags run: | @@ -277,11 +287,9 @@ jobs: - name: Calculate cache key for node modules id: calculate_node_modules_hash run: | - PACKAGE_JSON_HASH=$(cat package.json | jq '{resolutions,dependencies,devDependencies}' | sha1sum -t | cut -f1 -d" ") - echo "PACKAGE_JSON_HASH: $PACKAGE_JSON_HASH" - export NODE_MODULES_HASH=${{ runner.os }}-${{ hashFiles('yarn.lock') }}-$PACKAGE_JSON_HASH - echo "NODE_MODULES_HASH: $NODE_MODULES_HASH" - echo "node-modules-hash=$NODE_MODULES_HASH" >> $GITHUB_OUTPUT + HASH="$(./scripts/ci/get-node-modules-hash.mjs)" + echo "node-modules-hash: ${HASH}" + echo "node-modules-hash=${HASH}" >> $GITHUB_OUTPUT - name: Calculate cache keys for generated files id: calculate_generated_files_cache_key @@ -306,6 +314,15 @@ jobs: if: steps.node-modules.outputs.cache-hit != 'true' run: ./scripts/ci/10_prepare-host-deps.sh + - name: Set Test Everything true + run: | + echo "TEST_EVERYTHING=true" >> $GITHUB_ENV + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test everything') + + - name: Set Test Everything false + run: echo "TEST_EVERYTHING=false" >> $GITHUB_ENV + if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'test everything') + - name: Preparing BASE tags id: git_nx_base env: @@ -328,11 +345,6 @@ jobs: git checkout $GITHUB_SHA echo "BASE=$BASE" >> $GITHUB_ENV echo "LAST_GOOD_BUILD_DOCKER_TAG=${LAST_GOOD_BUILD_DOCKER_TAG}" >> $GITHUB_OUTPUT - - if [ -n "${NX_AFFECTED_ALL+x}" ]; then - echo "NX_AFFECTED_ALL=$NX_AFFECTED_ALL" >> $GITHUB_ENV - fi - - name: Docker login to ECR repo run: ./scripts/ci/docker-login-ecr.sh env: @@ -370,7 +382,8 @@ jobs: - name: Building NodeJS dependencies if: steps.cache-deps.outputs.cache-hit != 'true' || steps.cache-deps-base.outputs.cache-hit != 'true' - run: ./scripts/ci/10_prepare-docker-deps.sh + run: | + ./scripts/ci/10_prepare-docker-deps.sh - name: set BRANCH env var run: echo "BRANCH=$GIT_BRANCH" >> $GITHUB_ENV @@ -452,9 +465,9 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' - name: Setup yarn run: npm install -g yarn @@ -505,6 +518,7 @@ jobs: AFFECTED_ALL: ${{ secrets.AFFECTED_ALL }} GIT_BRANCH: ${{ needs.pre-checks.outputs.GIT_BRANCH}} DOCKER_TAG: ${{ needs.prepare.outputs.DOCKER_TAG}} + NODE_IMAGE_TAG: ${{ needs.prepare.outputs.NODE_IMAGE_TAG}} PUBLISH: true strategy: fail-fast: false @@ -519,22 +533,17 @@ jobs: echo "AFFECTED_PROJECTS=$AFFECTED_PROJECTS" >> $GITHUB_ENV echo "DOCKER_TYPE=$DOCKER_TYPE" >> $GITHUB_ENV continue-on-error: true - - uses: actions/setup-node@v3 - with: - node-version: '18' + - uses: actions/checkout@v3 if: steps.gather.outcome == 'success' - - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '18' + node-version-file: 'package.json' if: steps.gather.outcome == 'success' - name: Setup yarn run: npm install -g yarn if: steps.gather.outcome == 'success' - - uses: actions/checkout@v3 - if: steps.gather.outcome == 'success' - name: Cache for generated files id: generated-files-cache if: steps.gather.outcome == 'success' @@ -605,15 +614,25 @@ jobs: continue-on-error: true id: dockerbuild if: steps.gather.outcome == 'success' - run: ./scripts/ci/run-in-parallel.sh 90_$DOCKER_TYPE env: - EXTRA_DOCKER_BUILD_ARGS: '--build-arg DOCKER_IMAGE_REGISTRY=${{ env.DOCKER_BASE_IMAGE_REGISTRY }} --build-arg GIT_SHA=${{ github.sha }}' + NODE_IMAGE_TAG: ${{ needs.prepare.outputs.NODE_IMAGE_TAG }} + SHA: ${{ github.sha }} + DOCKER_BASE_IMAGE_REGISTRY: ${{ env.DOCKER_BASE_IMAGE_REGISTRY }} + run: | + echo Node image tag is: $NODE_IMAGE_TAG + export EXTRA_DOCKER_BUILD_ARGS="--build-arg DOCKER_IMAGE_REGISTRY=$DOCKER_BASE_IMAGE_REGISTRY --build-arg GIT_SHA=$SHA --build-arg NODE_IMAGE_TAG=$NODE_IMAGE_TAG" + ./scripts/ci/run-in-parallel.sh 90_$DOCKER_TYPE - - name: Building Docker images Retry #This only exists until GHA starts supporting this + - name: Building Docker images Retry if: steps.gather.outcome == 'success' && steps.dockerbuild.outcome == 'failure' - run: ./scripts/ci/run-in-parallel.sh 90_$DOCKER_TYPE env: - EXTRA_DOCKER_BUILD_ARGS: '--build-arg DOCKER_IMAGE_REGISTRY=${{ env.DOCKER_BASE_IMAGE_REGISTRY }} --build-arg GIT_SHA=${{ github.sha }}' + NODE_IMAGE_TAG: ${{ needs.prepare.outputs.NODE_IMAGE_TAG }} + SHA: ${{ github.sha }} + DOCKER_BASE_IMAGE_REGISTRY: ${{ env.DOCKER_BASE_IMAGE_REGISTRY }} + run: | + echo Node image tag is: $NODE_IMAGE_TAG + export EXTRA_DOCKER_BUILD_ARGS="--build-arg DOCKER_IMAGE_REGISTRY=$DOCKER_BASE_IMAGE_REGISTRY --build-arg GIT_SHA=$SHA --build-arg NODE_IMAGE_TAG=$NODE_IMAGE_TAG" + ./scripts/ci/run-in-parallel.sh 90_$DOCKER_TYPE helm-docker-build: needs: @@ -628,6 +647,7 @@ jobs: FEATURE_NAME: ${{ needs.pre-checks.outputs.FEATURE_NAME }} DOCKER_TAG: ${{ needs.prepare.outputs.DOCKER_TAG}} GIT_BRANCH: ${{ needs.pre-checks.outputs.GIT_BRANCH }} + NODE_IMAGE_TAG: ${{ needs.prepare.outputs.NODE_IMAGE_TAG }} steps: - uses: actions/checkout@v3 @@ -647,6 +667,9 @@ jobs: - name: Docker build image working-directory: infra run: | + echo Registry is: ${{env.DOCKER_BASE_IMAGE_REGISTRY}} + echo Image tag is: ${{env.NODE_IMAGE_TAG}} + export EXTRA_DOCKER_BUILD_ARGS="--build-arg DOCKER_IMAGE_REGISTRY=${{env.DOCKER_BASE_IMAGE_REGISTRY}} --build-arg NODE_IMAGE_TAG=${{env.NODE_IMAGE_TAG}}" ./scripts/build-docker-container.sh $DOCKER_TAG echo "COMMENT<> $GITHUB_ENV echo "Affected services are: ${{needs.prepare.outputs.IMAGES}}" >> $GITHUB_ENV @@ -654,7 +677,6 @@ jobs: echo 'EOF' >> $GITHUB_ENV env: PUBLISH: 'true' - EXTRA_DOCKER_BUILD_ARGS: '--build-arg DOCKER_IMAGE_REGISTRY=${{ env.DOCKER_BASE_IMAGE_REGISTRY }}' - name: Retag as latest if: ${{ env.GIT_BRANCH == 'main' && env.NX_AFFECTED_ALL != 'true' }} diff --git a/apps/api/infra/api.ts b/apps/api/infra/api.ts index aae3e246ab52..b68e752223ae 100644 --- a/apps/api/infra/api.ts +++ b/apps/api/infra/api.ts @@ -370,6 +370,7 @@ export const serviceSetup = (services: { '/k8s/api/WATSON_ASSISTANT_CHAT_FEEDBACK_API_KEY', LICENSE_SERVICE_BARCODE_SECRET_KEY: '/k8s/api/LICENSE_SERVICE_BARCODE_SECRET_KEY', + ULTRAVIOLET_RADIATION_API_KEY: '/k8s/api/ULTRAVIOLET_RADIATION_API_KEY', }) .xroad( AdrAndMachine, diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 054b9cd04ef0..3fbf0f5b2c94 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -184,6 +184,7 @@ import { } from '@island.is/clients/university-careers' import { HousingBenefitsConfig } from '@island.is/clients/hms-housing-benefits' import { UserProfileClientConfig } from '@island.is/clients/user-profile' +import { UltravioletRadiationClientConfig } from '@island.is/clients/ultraviolet-radiation' const environment = getConfig @@ -412,6 +413,7 @@ const environment = getConfig UniversityGatewayApiClientConfig, LicenseConfig, UserProfileClientConfig, + UltravioletRadiationClientConfig, ], }), ], diff --git a/apps/application-system/api/src/app/modules/application/admin.controller.ts b/apps/application-system/api/src/app/modules/application/admin.controller.ts index 14f3b6e245ce..73e8a219c90c 100644 --- a/apps/application-system/api/src/app/modules/application/admin.controller.ts +++ b/apps/application-system/api/src/app/modules/application/admin.controller.ts @@ -22,6 +22,7 @@ import { BypassDelegation } from './guards/bypass-delegation.decorator' import { ApplicationAdminPaginatedResponse, ApplicationListAdminResponseDto, + ApplicationStatistics, } from './dto/applicationAdmin.response.dto' import { ApplicationAdminSerializer } from './tools/applicationAdmin.serializer' @@ -42,6 +43,40 @@ export class AdminController { @Inject(LOGGER_PROVIDER) private logger: Logger, ) {} + @Scopes(AdminPortalScope.applicationSystemAdmin) + @BypassDelegation() + @Get('admin/applications-statistics') + @Documentation({ + description: 'Get applications statistics', + response: { + status: 200, + type: [ApplicationStatistics], + }, + request: { + query: { + startDate: { + type: 'string', + required: true, + description: 'Start date for the statistics', + }, + endDate: { + type: 'string', + required: true, + description: 'End date for the statistics', + }, + }, + }, + }) + async getCountByTypeIdAndStatus( + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + ) { + return this.applicationService.getApplicationCountByTypeIdAndStatus( + startDate, + endDate, + ) + } + @Scopes(AdminPortalScope.applicationSystemAdmin) @BypassDelegation() @Get('admin/:nationalId/applications') @@ -94,6 +129,7 @@ export class AdminController { true, // Show pruned applications ) } + @Scopes(AdminPortalScope.applicationSystemInstitution) @BypassDelegation() @Get('admin/institution/:nationalId/applications/:page/:count') diff --git a/apps/application-system/api/src/app/modules/application/dto/applicationAdmin.response.dto.ts b/apps/application-system/api/src/app/modules/application/dto/applicationAdmin.response.dto.ts index 4da2f71de4f6..2249a4aec957 100644 --- a/apps/application-system/api/src/app/modules/application/dto/applicationAdmin.response.dto.ts +++ b/apps/application-system/api/src/app/modules/application/dto/applicationAdmin.response.dto.ts @@ -41,3 +41,40 @@ export class ApplicationAdminPaginatedResponse { @IsNumber() count!: number } + +export class ApplicationStatistics { + @ApiProperty() + @Expose() + @IsString() + typeid!: string + + @ApiProperty() + @Expose() + @IsNumber() + count!: number + + @ApiProperty() + @Expose() + @IsNumber() + draft!: number + + @ApiProperty() + @Expose() + @IsNumber() + inprogress!: number + + @ApiProperty() + @Expose() + @IsNumber() + completed!: number + + @ApiProperty() + @Expose() + @IsNumber() + rejected!: number + + @ApiProperty() + @Expose() + @IsNumber() + approved!: number +} diff --git a/apps/auth-admin-web/components/Client/form/ClientCreateForm.tsx b/apps/auth-admin-web/components/Client/form/ClientCreateForm.tsx index f5e56ff7a53e..17871cbbbdda 100644 --- a/apps/auth-admin-web/components/Client/form/ClientCreateForm.tsx +++ b/apps/auth-admin-web/components/Client/form/ClientCreateForm.tsx @@ -712,149 +712,16 @@ const ClientCreateForm: React.FC> = (

{localization.sections['delegations'].title}

-
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
+ https://island.is/stjornbord/innskraningarkerfi +
+

diff --git a/apps/auth-admin-web/components/Resource/forms/ApiScopeCreateForm.tsx b/apps/auth-admin-web/components/Resource/forms/ApiScopeCreateForm.tsx index dde72376e27b..8db1effd7312 100644 --- a/apps/auth-admin-web/components/Resource/forms/ApiScopeCreateForm.tsx +++ b/apps/auth-admin-web/components/Resource/forms/ApiScopeCreateForm.tsx @@ -449,186 +449,16 @@ const ApiScopeCreateForm: React.FC> = (

{localization.sections['delegations'].title}

-
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
-
- - - -
- -
- - - -
+ https://island.is/stjornbord/innskraningarkerfi +
+

diff --git a/apps/financial-aid/api/src/app/modules/application/dto/updateApplication.input.ts b/apps/financial-aid/api/src/app/modules/application/dto/updateApplication.input.ts index b8094e23e5f0..d69d731f9512 100644 --- a/apps/financial-aid/api/src/app/modules/application/dto/updateApplication.input.ts +++ b/apps/financial-aid/api/src/app/modules/application/dto/updateApplication.input.ts @@ -24,6 +24,10 @@ export class UpdateApplicationInput implements UpdateApplication { @Field(() => String) readonly event!: ApplicationEventType + @Allow() + @Field({ nullable: true }) + readonly appliedDate?: string + @Allow() @Field({ nullable: true }) readonly rejection?: string @@ -58,7 +62,7 @@ export class UpdateApplicationInput implements UpdateApplication { @Allow() @Field({ nullable: true }) - readonly spouseHasFetchedDirectTaxPayment!: boolean + readonly spouseHasFetchedDirectTaxPayment?: boolean @Allow() @Field(() => [DirectTaxPaymentInput], { nullable: true }) diff --git a/apps/financial-aid/api/src/app/modules/application/models/application.model.ts b/apps/financial-aid/api/src/app/modules/application/models/application.model.ts index a5c584c9008a..6ab17b7b2692 100644 --- a/apps/financial-aid/api/src/app/modules/application/models/application.model.ts +++ b/apps/financial-aid/api/src/app/modules/application/models/application.model.ts @@ -26,6 +26,9 @@ export class ApplicationModel implements Application { @Field() readonly modified!: string + @Field() + readonly appliedDate!: string + @Field() readonly nationalId!: string diff --git a/apps/financial-aid/backend/migrations/20240528122439-update-application-add-applied.js b/apps/financial-aid/backend/migrations/20240528122439-update-application-add-applied.js new file mode 100644 index 000000000000..ebecf13be552 --- /dev/null +++ b/apps/financial-aid/backend/migrations/20240528122439-update-application-add-applied.js @@ -0,0 +1,48 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async (t) => { + // Add the new column + await queryInterface.addColumn( + 'applications', + 'applied', + { + type: Sequelize.DATE, + allowNull: true, + }, + { transaction: t }, + ) + + // Update the applied column with the createdAt date + await queryInterface.sequelize.query( + ` + UPDATE applications + SET applied = created; + `, + { transaction: t }, + ) + + // Change the column to not allow null values if necessary + await queryInterface.changeColumn( + 'applications', + 'applied', + { + type: Sequelize.DATE, + allowNull: false, + }, + { transaction: t }, + ) + }) + }, + + async down(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.removeColumn('applications', 'applied', { + transaction: t, + }), + ]), + ) + }, +} diff --git a/apps/financial-aid/backend/migrations/20240529131132-application_event-add-to-event_type.js b/apps/financial-aid/backend/migrations/20240529131132-application_event-add-to-event_type.js new file mode 100644 index 000000000000..7edcca5591b0 --- /dev/null +++ b/apps/financial-aid/backend/migrations/20240529131132-application_event-add-to-event_type.js @@ -0,0 +1,33 @@ +'use strict' + +const replaceEnum = require('sequelize-replace-enum-postgres').default + +module.exports = { + up: async (queryInterface, Sequelize) => { + return replaceEnum({ + queryInterface, + tableName: 'application_events', + columnName: 'event_type', + defaultValue: 'New', + newValues: [ + 'SpouseFileUpload', + 'New', + 'InProgress', + 'DataNeeded', + 'Rejected', + 'Approved', + 'StaffComment', + 'UserComment', + 'FileUpload', + 'AssignCase', + 'DateChanged', + ], + enumName: 'enum_application_events_event_type', + }) + }, + + down: async (queryInterface, Sequelize) => { + // no need to roll back + return + }, +} diff --git a/apps/financial-aid/backend/migrations/20240531145432-update-application-applied_date.js b/apps/financial-aid/backend/migrations/20240531145432-update-application-applied_date.js new file mode 100644 index 000000000000..720d46d2103a --- /dev/null +++ b/apps/financial-aid/backend/migrations/20240531145432-update-application-applied_date.js @@ -0,0 +1,23 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.renameColumn('applications', 'applied', 'applied_date', { + transaction: t, + }), + ]), + ) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.renameColumn('applications', 'applied_date', 'applied', { + transaction: t, + }), + ]), + ) + }, +} diff --git a/apps/financial-aid/backend/src/app/modules/application/application.controller.ts b/apps/financial-aid/backend/src/app/modules/application/application.controller.ts index 3f15a3de9bdf..6600fb51675a 100644 --- a/apps/financial-aid/backend/src/app/modules/application/application.controller.ts +++ b/apps/financial-aid/backend/src/app/modules/application/application.controller.ts @@ -246,6 +246,7 @@ export class ApplicationController { ApplicationEventType.INPROGRESS, ApplicationEventType.ASSIGNCASE, ApplicationEventType.NEW, + ApplicationEventType.DATECHANGED, ] const applicantUpdateEvents = [ diff --git a/apps/financial-aid/backend/src/app/modules/application/application.service.ts b/apps/financial-aid/backend/src/app/modules/application/application.service.ts index ba8d96c9f803..d5b7b7b63d2d 100644 --- a/apps/financial-aid/backend/src/app/modules/application/application.service.ts +++ b/apps/financial-aid/backend/src/app/modules/application/application.service.ts @@ -55,6 +55,7 @@ import { DeductionFactorsModel } from '../deductionFactors' import { DirectTaxPaymentService } from '../directTaxPayment' import { DirectTaxPaymentModel } from '../directTaxPayment/models' import { ChildrenModel, ChildrenService } from '../children' +import { nowFactory } from './factories/date.factory' interface Recipient { name: string @@ -146,7 +147,7 @@ export class ApplicationService { spouseNationalId: nationalId, }, ], - created: { [Op.gte]: firstDateOfMonth() }, + appliedDate: { [Op.gte]: firstDateOfMonth() }, }, }) @@ -294,6 +295,7 @@ export class ApplicationService { const appModel = await this.applicationModel.create({ ...application, + appliedDate: nowFactory(), nationalId: application.nationalId || user.nationalId, }) diff --git a/apps/financial-aid/backend/src/app/modules/application/dto/updateApplication.dto.ts b/apps/financial-aid/backend/src/app/modules/application/dto/updateApplication.dto.ts index 7d86c57379f7..75b7f3f3047e 100644 --- a/apps/financial-aid/backend/src/app/modules/application/dto/updateApplication.dto.ts +++ b/apps/financial-aid/backend/src/app/modules/application/dto/updateApplication.dto.ts @@ -19,13 +19,18 @@ export class UpdateApplicationDto { @IsOptional() @IsString() @ApiProperty() - readonly state: ApplicationState + readonly state?: ApplicationState @IsNotEmpty() @IsString() @ApiProperty() readonly event: ApplicationEventType + @IsOptional() + @IsString() + @ApiProperty() + readonly appliedDate?: string + @IsOptional() @IsString() @ApiProperty() diff --git a/apps/financial-aid/backend/src/app/modules/application/factories/date.factory.ts b/apps/financial-aid/backend/src/app/modules/application/factories/date.factory.ts new file mode 100644 index 000000000000..d6805340f92e --- /dev/null +++ b/apps/financial-aid/backend/src/app/modules/application/factories/date.factory.ts @@ -0,0 +1 @@ +export const nowFactory = () => new Date() diff --git a/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts b/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts index 049dd7656769..9bbbbda00889 100644 --- a/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts +++ b/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts @@ -49,6 +49,13 @@ export class ApplicationModel extends Model { @ApiProperty() modified: Date + @Column({ + type: DataType.DATE, + allowNull: false, + }) + @ApiProperty() + appliedDate: Date + @Column({ type: DataType.STRING, allowNull: false, diff --git a/apps/financial-aid/backend/src/app/modules/application/test/create.spec.ts b/apps/financial-aid/backend/src/app/modules/application/test/create.spec.ts index 90509e2f2711..832cef72cd5a 100644 --- a/apps/financial-aid/backend/src/app/modules/application/test/create.spec.ts +++ b/apps/financial-aid/backend/src/app/modules/application/test/create.spec.ts @@ -1,6 +1,7 @@ import { EmailService } from '@island.is/email-service' import { ApplicationState, + ChildrenAid, Employment, FamilyStatus, FileType, @@ -21,6 +22,8 @@ import { ApplicationModel } from '../models/application.model' import { createTestingApplicationModule } from './createTestingApplicationModule' import { DirectTaxPaymentService } from '../../directTaxPayment' import { ChildrenService } from '../../children' +import { nowFactory } from '../factories/date.factory' +jest.mock('../factories/date.factory') interface Then { result: ApplicationModel @@ -80,6 +83,7 @@ describe('ApplicationController - Create', () => { describe('database query', () => { let mockCreate: jest.Mock let mockFindOne: jest.Mock + const date = new Date() const user: User = { nationalId: '0000000000', @@ -121,6 +125,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } beforeEach(async () => { @@ -129,6 +135,9 @@ describe('ApplicationController - Create', () => { const mockFindApplication = mockApplicationModel.findOne as jest.Mock mockFindApplication.mockReturnValueOnce(null) + const mockToday = nowFactory as jest.Mock + mockToday.mockReturnValueOnce(date) + await givenWhenThen(user, application) }) @@ -136,6 +145,7 @@ describe('ApplicationController - Create', () => { expect(mockCreate).toHaveBeenCalledWith({ nationalId: user.nationalId, ...application, + appliedDate: date, }) }) @@ -150,7 +160,7 @@ describe('ApplicationController - Create', () => { spouseNationalId: user.nationalId, }, ], - created: { [Op.gte]: firstDateOfMonth() }, + appliedDate: { [Op.gte]: firstDateOfMonth() }, }, }) }) @@ -201,6 +211,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } const municipality: Municipality = { @@ -213,12 +225,14 @@ describe('ApplicationController - Create', () => { individualAid: undefined, cohabitationAid: undefined, usingNav: false, + childrenAid: ChildrenAid.NOTDEFINED, } const appModel = { id, state: application.state, created: new Date(), + appliedDate: new Date(), email: application.email, } @@ -323,6 +337,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: null, nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } const municipality: Municipality = { @@ -335,12 +351,14 @@ describe('ApplicationController - Create', () => { individualAid: undefined, cohabitationAid: undefined, usingNav: false, + childrenAid: ChildrenAid.NOTDEFINED, } const appModel = { id, state: application.state, created: new Date(), + appliedDate: new Date(), email: application.email, spouseEmail: application.spouseEmail, spouseName: application.spouseName, @@ -437,12 +455,15 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } const appModel = { id, state: application.state, created: new Date(), + appliedDate: new Date(), email: application.email, } @@ -532,6 +553,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: '', spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } const user: User = { nationalId: '0000000000', @@ -542,6 +565,7 @@ describe('ApplicationController - Create', () => { id, state: application.state, created: new Date(), + appliedDate: new Date(), email: application.email, } @@ -575,7 +599,7 @@ describe('ApplicationController - Create', () => { }) }) - describe('applicant has applied for period', () => { + describe('applicant has appliedDate for period', () => { let then: Then const id = uuid() @@ -623,13 +647,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, - } - - const appModel = { - id, - state: application.state, - created: new Date(), - email: application.email, + children: [], + childrenComment: '', } beforeEach(async () => { @@ -687,6 +706,8 @@ describe('ApplicationController - Create', () => { applicationSystemId: '', nationalId: user.nationalId, spouseHasFetchedDirectTaxPayment: false, + children: [], + childrenComment: '', } beforeEach(async () => { diff --git a/apps/financial-aid/backend/src/app/modules/application/test/getCurrentApplication.spec.ts b/apps/financial-aid/backend/src/app/modules/application/test/getCurrentApplication.spec.ts index 83fb32bb64f3..92416fbb4080 100644 --- a/apps/financial-aid/backend/src/app/modules/application/test/getCurrentApplication.spec.ts +++ b/apps/financial-aid/backend/src/app/modules/application/test/getCurrentApplication.spec.ts @@ -56,7 +56,7 @@ describe('ApplicationController - Get current application', () => { spouseNationalId: user.nationalId, }, ], - created: { [Op.gte]: firstDateOfMonth() }, + appliedDate: { [Op.gte]: firstDateOfMonth() }, }, }) }) diff --git a/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts b/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts index b39f4d3679d4..a00390154dd4 100644 --- a/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts +++ b/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts @@ -316,6 +316,7 @@ describe('ApplicationController - Update', () => { ${ApplicationEventType.APPROVED} ${ApplicationEventType.STAFFCOMMENT} ${ApplicationEventType.ASSIGNCASE} + ${ApplicationEventType.DATECHANGED} ${ApplicationEventType.NEW} ${ApplicationEventType.INPROGRESS} `.describe('$event', ({ event }) => { @@ -423,6 +424,7 @@ describe('ApplicationController - Update', () => { ${ApplicationEventType.APPROVED} ${ApplicationEventType.STAFFCOMMENT} ${ApplicationEventType.ASSIGNCASE} + ${ApplicationEventType.DATECHANGED} ${ApplicationEventType.NEW} ${ApplicationEventType.INPROGRESS} `.describe('$event', ({ event }) => { diff --git a/apps/financial-aid/web-veita/graphql/sharedGql.ts b/apps/financial-aid/web-veita/graphql/sharedGql.ts index ce29ec6a418e..482dd0b7a15b 100644 --- a/apps/financial-aid/web-veita/graphql/sharedGql.ts +++ b/apps/financial-aid/web-veita/graphql/sharedGql.ts @@ -7,6 +7,7 @@ export const ApplicationQuery = gql` applicationSystemId nationalId created + appliedDate modified name phoneNumber @@ -150,6 +151,7 @@ export const UpdateApplicationTableMutation = gql` email modified created + appliedDate state staff { name @@ -178,6 +180,7 @@ export const ApplicationsQuery = gql` email modified created + appliedDate state staff { name @@ -208,6 +211,7 @@ export const ApplicationEventMutation = gql` nationalId created modified + appliedDate name phoneNumber email @@ -328,6 +332,7 @@ export const UpdateApplicationMutation = gql` nationalId created modified + appliedDate name phoneNumber email diff --git a/apps/financial-aid/web-veita/src/components/ApplicationHeader/ApplicationHeader.tsx b/apps/financial-aid/web-veita/src/components/ApplicationHeader/ApplicationHeader.tsx index 49402de5718e..99098905f673 100644 --- a/apps/financial-aid/web-veita/src/components/ApplicationHeader/ApplicationHeader.tsx +++ b/apps/financial-aid/web-veita/src/components/ApplicationHeader/ApplicationHeader.tsx @@ -47,10 +47,10 @@ const ApplicationHeader = ({ await changeApplicationState( application.id, + ApplicationEventType.ASSIGNCASE, application.state === ApplicationState.NEW ? ApplicationState.INPROGRESS : application.state, - ApplicationEventType.ASSIGNCASE, ) .then((updatedApplication) => { setApplication(updatedApplication) diff --git a/apps/financial-aid/web-veita/src/components/ApplicationsTable/ApplicationsTable.tsx b/apps/financial-aid/web-veita/src/components/ApplicationsTable/ApplicationsTable.tsx index 488baf81489b..490b4dc66a40 100644 --- a/apps/financial-aid/web-veita/src/components/ApplicationsTable/ApplicationsTable.tsx +++ b/apps/financial-aid/web-veita/src/components/ApplicationsTable/ApplicationsTable.tsx @@ -22,6 +22,7 @@ import { getStateUrlFromRoute, Routes, SortableTableHeaderProps, + truncateString, } from '@island.is/financial-aid/shared/lib' import { useAllApplications } from '@island.is/financial-aid-web/veita/src/utils/useAllApplications' @@ -81,7 +82,7 @@ const ApplicationsTable = ({ <> {application.staff?.name ? ( - {application.staff?.name} + {truncateString(application.staff?.name, 13)} ) : ( @@ -145,7 +146,7 @@ const ApplicationsTable = ({ ), TextTableItem( 'default', - getMonth(new Date(item.created).getMonth()), + getMonth(new Date(item.appliedDate).getMonth()), ), assignButton(item), ]} diff --git a/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.css.ts b/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.css.ts new file mode 100644 index 000000000000..120820488ccb --- /dev/null +++ b/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css' + +export const modal = style({ + display: 'block', + width: '100%', + maxWidth: '440px', + boxShadow: '0px 8px 32px rgba(0, 0, 0, 0.08)', + borderRadius: '12px', +}) diff --git a/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.tsx b/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.tsx new file mode 100644 index 000000000000..c9611cc7bdc1 --- /dev/null +++ b/apps/financial-aid/web-veita/src/components/AppliedMonthModal/AppliedMonthModal.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react' +import { ModalBase, Text, Box } from '@island.is/island-ui/core' +import format from 'date-fns/format' + +import * as styles from './AppliedMonthModal.css' +import * as modalButtonStyles from '../ModalTypes/ModalTypes.css' +import cn from 'classnames' + +import * as modalStyles from '../StateModal/StateModal.css' + +import { + Application, + ApplicationEventType, + getMonth, +} from '@island.is/financial-aid/shared/lib' +import { useApplicationState } from '@island.is/financial-aid-web/veita/src/utils/useApplicationState' + +interface Props { + headline: string + isVisible: boolean + onVisibilityChange: React.Dispatch> + appliedDate: string + createdDate: string + applicationId: string + setApplication: React.Dispatch> +} + +const AppliedMonthModal = ({ + headline, + isVisible, + onVisibilityChange, + appliedDate, + createdDate, + applicationId, + setApplication, +}: Props) => { + const closeModal = (): void => { + onVisibilityChange(false) + } + const currentAppliedMonth = new Date(appliedDate).getMonth() + + const [error, setError] = useState(false) + + const getSurroundingMonths = (createdDate: string): Date[] => { + const date = new Date(createdDate) + + if (Number.isNaN(date.getTime())) { + throw new Error('Invalid date') + } + + const year = date.getFullYear() + const month = date.getMonth() + + // Calculate the previous two months + const prevMonth1 = new Date(year, month - 1) + const prevMonth2 = new Date(year, month - 2) + + // Calculate the next month + const nextMonth = new Date(year, month + 1) + + return [prevMonth2, prevMonth1, date, nextMonth] + } + + const updateApplication = useApplicationState() + + const onClickUpdateAppliedMonth = async (newDate: Date) => { + await updateApplication( + applicationId, + ApplicationEventType.DATECHANGED, + undefined, + newDate, + undefined, + `Tímabilið var breytt frá ${getMonth(currentAppliedMonth)} í ${getMonth( + new Date(newDate).getMonth(), + )}`, + ) + .then((updatedApplication) => { + setApplication(updatedApplication) + onVisibilityChange(false) + }) + .catch(() => { + setError(true) + }) + } + + return ( + { + if (visibility !== isVisible) { + onVisibilityChange(visibility) + } + }} + className={modalStyles.modalBase} + > + + + + + + + {headline} + + + + + + {getSurroundingMonths(createdDate).map((surroundingMonth) => { + const date = new Date(surroundingMonth) + const isActive = date.getMonth() === currentAppliedMonth + + return ( + + ) + })} + +
+ + Eitthvað misstókst, vinsamlegast reyndu aftur síðar + +
+
+
+
+
+ ) +} + +export default AppliedMonthModal diff --git a/apps/financial-aid/web-veita/src/components/CommentSection/CommentSection.tsx b/apps/financial-aid/web-veita/src/components/CommentSection/CommentSection.tsx index cd2528ca8e32..d59e52975cc5 100644 --- a/apps/financial-aid/web-veita/src/components/CommentSection/CommentSection.tsx +++ b/apps/financial-aid/web-veita/src/components/CommentSection/CommentSection.tsx @@ -1,54 +1,47 @@ import React, { useContext, useState } from 'react' -import { useRouter } from 'next/router' - import { Box, Input, Button } from '@island.is/island-ui/core' -import { useMutation } from '@apollo/client' -import { ApplicationEventMutation } from '@island.is/financial-aid-web/veita/graphql/sharedGql' import { Application, ApplicationEventType, } from '@island.is/financial-aid/shared/lib' -import { AdminContext } from '../AdminProvider/AdminProvider' import AnimateHeight from 'react-animate-height' +import { useApplicationEvent } from '@island.is/financial-aid-web/veita/src/utils/useApplicationEvent' +import { AdminContext } from '@island.is/financial-aid-web/veita/src/components/AdminProvider/AdminProvider' interface Props { - className?: string + applicationId: string setApplication: React.Dispatch> + className?: string } -const CommentSection = ({ className, setApplication }: Props) => { - const router = useRouter() - +const CommentSection = ({ + className, + setApplication, + applicationId, +}: Props) => { const { admin } = useContext(AdminContext) + const [showInput, setShowInput] = useState(false) const [comment, setComment] = useState() - const [ - createApplicationEventMutation, - { loading: isCreatingApplicationEvent }, - ] = useMutation(ApplicationEventMutation) + const { isCreatingApplicationEvent, creatApplicationEvent } = + useApplicationEvent() - const saveStaffComment = async (staffComment: string | undefined) => { - if (staffComment) { - const { data } = await createApplicationEventMutation({ - variables: { - input: { - applicationId: router.query.id, - comment: staffComment, - eventType: ApplicationEventType.STAFFCOMMENT, - staffNationalId: admin?.nationalId, - staffName: admin?.name, - }, - }, - }) + const onClickComment = async () => { + const updatedApplication = await creatApplicationEvent( + applicationId, + ApplicationEventType.STAFFCOMMENT, + admin?.nationalId, + admin?.name, + comment, + ) - if (data) { - setApplication(data.createApplicationEvent) - setComment('') - setShowInput(false) - } + if (updatedApplication) { + setApplication(updatedApplication) + setComment('') + setShowInput(false) } } @@ -84,9 +77,7 @@ const CommentSection = ({ className, setApplication }: Props) => { icon="checkmark" size="small" iconType="outline" - onClick={() => { - saveStaffComment(comment) - }} + onClick={onClickComment} disabled={isCreatingApplicationEvent} > Vista athugasemd diff --git a/apps/financial-aid/web-veita/src/components/ModalTypes/AcceptModal.tsx b/apps/financial-aid/web-veita/src/components/ModalTypes/AcceptModal.tsx index 0c7acc67dcd7..87177926f496 100644 --- a/apps/financial-aid/web-veita/src/components/ModalTypes/AcceptModal.tsx +++ b/apps/financial-aid/web-veita/src/components/ModalTypes/AcceptModal.tsx @@ -34,7 +34,7 @@ interface Props { interface calculationsState { amount: number income?: number - childrenAidAmount: number + childrenAidAmount?: number personalTaxCreditPercentage?: number secondPersonalTaxCredit: number showSecondPersonalTaxCredit: boolean @@ -56,6 +56,10 @@ const AcceptModal = ({ const router = useRouter() const maximumInputLength = 6 + const hasChildrenAid = + applicationMunicipality.childrenAid === ChildrenAid.APPLICANT && + hasApplicantChildren + const aidAmount = useMemo(() => { if (applicationMunicipality && homeCircumstances) { return aidCalculator( @@ -81,7 +85,7 @@ const AcceptModal = ({ const [state, setState] = useState({ amount: aidAmount, - childrenAidAmount: 0, + childrenAidAmount: undefined, income: undefined, personalTaxCreditPercentage: undefined, secondPersonalTaxCredit: 0, @@ -101,7 +105,7 @@ const AcceptModal = ({ const finalAmount = calculateAcceptedAidFinalAmount( state.amount + - state.childrenAidAmount - + (state.childrenAidAmount ?? 0) - checkingValue(state.income) - sumValues, checkingValue(state.personalTaxCreditPercentage), @@ -118,6 +122,7 @@ const AcceptModal = ({ const areRequiredFieldsFilled = state.income === undefined || state.personalTaxCreditPercentage === undefined || + (hasChildrenAid && state.childrenAidAmount === undefined) || !finalAmount || finalAmount === 0 @@ -167,26 +172,30 @@ const AcceptModal = ({ />
- {applicationMunicipality.childrenAid === ChildrenAid.APPLICANT && - hasApplicantChildren && ( - - { - setState({ - ...state, - childrenAidAmount: input, - hasError: false, - }) - }} - maximumInputLength={maximumInputLength} - /> - - )} + {hasChildrenAid && ( + + { + setState({ + ...state, + childrenAidAmount: input, + hasError: false, + }) + }} + maximumInputLength={maximumInputLength} + hasError={state.hasError && state.childrenAidAmount === undefined} + /> + + )} { const [isStateModalVisible, setStateModalVisible] = useState(false) + const [appliedMonthModalVisible, setAppliedMonthModalVisible] = + useState(false) const [isRejectedReasonModalVisible, setRejectedReasonModalVisible] = useState(false) @@ -87,15 +90,19 @@ const ApplicationProfile = ({ const applicationInfo: ApplicationProfileInfo[] = [ { - title: 'Tímabil', - content: - getMonth(new Date(application.created).getMonth()) + - format(new Date(application.created), ' y'), + title: 'Dagsetning umsóknar', + content: format(new Date(application.created), 'dd.MM.y · kk:mm'), }, { - title: 'Sótt um', - content: format(new Date(application.created), 'dd.MM.y · kk:mm'), + title: 'Fyrir tímabilið', + content: + getMonth(new Date(application.appliedDate).getMonth()) + + format(new Date(application.appliedDate), ' y'), + onclick: () => { + setAppliedMonthModalVisible(true) + }, }, + aidAmount ? { title: 'Áætluð aðstoð', @@ -274,6 +281,7 @@ const ApplicationProfile = ({ {!isPrint && ( @@ -322,6 +330,18 @@ const ApplicationProfile = ({ }} reason={application.rejection ?? ''} /> + + { + setAppliedMonthModalVisible(isVisibleBoolean) + }} + appliedDate={application.appliedDate} + createdDate={application.created} + applicationId={application.id} + setApplication={setApplication} + /> ) } diff --git a/apps/financial-aid/web-veita/src/components/StateModal/StateModal.tsx b/apps/financial-aid/web-veita/src/components/StateModal/StateModal.tsx index ef36961a82f3..37039684f873 100644 --- a/apps/financial-aid/web-veita/src/components/StateModal/StateModal.tsx +++ b/apps/financial-aid/web-veita/src/components/StateModal/StateModal.tsx @@ -66,8 +66,9 @@ const StateModal = ({ await changeApplicationState( applicationId, - state, eventTypeFromApplicationState[state], + state, + undefined, rejection, comment, amount, diff --git a/apps/financial-aid/web-veita/src/components/index.ts b/apps/financial-aid/web-veita/src/components/index.ts index 35a2195d5574..25d39400edad 100644 --- a/apps/financial-aid/web-veita/src/components/index.ts +++ b/apps/financial-aid/web-veita/src/components/index.ts @@ -15,6 +15,8 @@ export { default as OptionsModal } from './ModalTypes/OptionsModal' export { default as EmailFormatInputModal } from './ModalTypes/EmailFormatInputModal' export { default as AcceptModal } from './ModalTypes/AcceptModal' export { default as AidAmountModal } from './AidAmountModal/AidAmountModal' +export { default as AppliedMonthModal } from './AppliedMonthModal/AppliedMonthModal' + export { default as TableHeaders } from './TableHeaders/TableHeaders' export { default as SortableTableHeader } from './TableHeaders/SortableTableHeader' diff --git a/apps/financial-aid/web-veita/src/utils/navigation.ts b/apps/financial-aid/web-veita/src/utils/navigation.ts index 85738753fecd..f83331caefe6 100644 --- a/apps/financial-aid/web-veita/src/utils/navigation.ts +++ b/apps/financial-aid/web-veita/src/utils/navigation.ts @@ -13,10 +13,10 @@ export const navigationItems = [ { title: 'Nafn', sortBy: ApplicationHeaderSortByEnum.NAME }, { title: 'Staða', sortBy: ApplicationHeaderSortByEnum.STATE }, { title: 'Tími án umsjár', sortBy: ApplicationHeaderSortByEnum.MODIFIED }, - { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.CREATED }, + { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.APPLIEDDATE }, { title: 'Umsjá', sortBy: ApplicationHeaderSortByEnum.STAFF }, ], - defaultHeaderSort: ApplicationHeaderSortByEnum.CREATED, + defaultHeaderSort: ApplicationHeaderSortByEnum.APPLIEDDATE, }, { group: 'Mitt', @@ -27,7 +27,7 @@ export const navigationItems = [ { title: 'Nafn', sortBy: ApplicationHeaderSortByEnum.NAME }, { title: 'Staða', sortBy: ApplicationHeaderSortByEnum.STATE }, { title: 'Síðast uppfært', sortBy: ApplicationHeaderSortByEnum.MODIFIED }, - { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.CREATED }, + { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.APPLIEDDATE }, { title: 'Unnið af', sortBy: ApplicationHeaderSortByEnum.STAFF }, ], defaultHeaderSort: ApplicationHeaderSortByEnum.MODIFIED, @@ -44,7 +44,7 @@ export const navigationItems = [ { title: 'Nafn', sortBy: ApplicationHeaderSortByEnum.NAME }, { title: 'Staða', sortBy: ApplicationHeaderSortByEnum.STATE }, { title: 'Úrlausnartími', sortBy: ApplicationHeaderSortByEnum.MODIFIED }, - { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.CREATED }, + { title: 'Sótt um', sortBy: ApplicationHeaderSortByEnum.CREATED }, { title: 'Unnið af', sortBy: ApplicationHeaderSortByEnum.STAFF }, ], defaultHeaderSort: ApplicationHeaderSortByEnum.MODIFIED, @@ -60,7 +60,7 @@ export const navigationItems = [ { title: 'Nafn', sortBy: ApplicationHeaderSortByEnum.NAME }, { title: 'Staða', sortBy: ApplicationHeaderSortByEnum.STATE }, { title: 'Úrlausnartími', sortBy: ApplicationHeaderSortByEnum.MODIFIED }, - { title: 'Tímabil', sortBy: ApplicationHeaderSortByEnum.CREATED }, + { title: 'Sótt um', sortBy: ApplicationHeaderSortByEnum.CREATED }, { title: 'Unnið af', sortBy: ApplicationHeaderSortByEnum.STAFF }, ], defaultHeaderSort: ApplicationHeaderSortByEnum.MODIFIED, diff --git a/apps/financial-aid/web-veita/src/utils/useApplicationEvent.ts b/apps/financial-aid/web-veita/src/utils/useApplicationEvent.ts new file mode 100644 index 000000000000..437c6c0d1d8b --- /dev/null +++ b/apps/financial-aid/web-veita/src/utils/useApplicationEvent.ts @@ -0,0 +1,39 @@ +import { useMutation } from '@apollo/client' +import { ApplicationEventType } from '@island.is/financial-aid/shared/lib' +import { ApplicationEventMutation } from '@island.is/financial-aid-web/veita/graphql/sharedGql' + +export const useApplicationEvent = () => { + const [ + createApplicationEventMutation, + { loading: isCreatingApplicationEvent }, + ] = useMutation(ApplicationEventMutation) + + const creatApplicationEvent = async ( + applicationId: string, + eventType: ApplicationEventType, + staffNationalId?: string, + staffName?: string, + staffComment?: string, + ) => { + const { data } = await createApplicationEventMutation({ + variables: { + input: { + applicationId: applicationId, + comment: staffComment, + eventType: eventType, + staffNationalId: staffNationalId, + staffName: staffName, + }, + }, + }) + + if (data) { + return data.createApplicationEvent + } + } + + return { + isCreatingApplicationEvent, + creatApplicationEvent, + } +} diff --git a/apps/financial-aid/web-veita/src/utils/useApplicationState.ts b/apps/financial-aid/web-veita/src/utils/useApplicationState.ts index c279b4dc4a91..351b472e3343 100644 --- a/apps/financial-aid/web-veita/src/utils/useApplicationState.ts +++ b/apps/financial-aid/web-veita/src/utils/useApplicationState.ts @@ -34,10 +34,11 @@ export const useApplicationState = () => { } } - const changeApplicationState = async ( + const updateApplication = async ( applicationId: string, - state: ApplicationState, event: ApplicationEventType, + state?: ApplicationState, + appliedDate?: Date, rejection?: string, comment?: string, amount?: Amount, @@ -48,9 +49,10 @@ export const useApplicationState = () => { input: { id: applicationId, state, + appliedDate, rejection, comment, - staffId: admin?.staff?.id, + staffId: appliedDate ? undefined : admin?.staff?.id, event, amount, }, @@ -64,5 +66,5 @@ export const useApplicationState = () => { } } - return changeApplicationState + return updateApplication } diff --git a/apps/judicial-system/api/src/app/modules/auth/auth.service.ts b/apps/judicial-system/api/src/app/modules/auth/auth.service.ts index 47dfeaf72b17..9f2ea5e7b278 100644 --- a/apps/judicial-system/api/src/app/modules/auth/auth.service.ts +++ b/apps/judicial-system/api/src/app/modules/auth/auth.service.ts @@ -160,7 +160,7 @@ export class AuthService { nationalId: string } } catch (error) { - console.error('Token verification failed:', error) + this.logger.error('Token verification failed:', error) throw error } } diff --git a/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts b/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts index fda810ed9962..e087ad955c7f 100644 --- a/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts +++ b/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts @@ -5,6 +5,7 @@ import { CaseAppealRulingDecision, CaseAppealState, CaseDecision, + CaseIndictmentRulingDecision, CaseState, CaseType, IndictmentCaseReviewDecision, @@ -120,4 +121,7 @@ export class CaseListEntry { @Field(() => String, { nullable: true }) readonly indictmentVerdictAppealDeadline?: string + + @Field(() => CaseIndictmentRulingDecision, { nullable: true }) + readonly indictmentRulingDecision?: CaseIndictmentRulingDecision } diff --git a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts index 8b179cee53a5..a77d184629b5 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts @@ -4,6 +4,7 @@ import { Field, ID, InputType } from '@nestjs/graphql' import { DefendantPlea, + DefenderChoice, Gender, ServiceRequirement, } from '@island.is/judicial-system/types' @@ -68,11 +69,6 @@ export class UpdateDefendantInput { @Field(() => String, { nullable: true }) readonly defenderPhoneNumber?: string - @Allow() - @IsOptional() - @Field(() => Boolean, { nullable: true }) - readonly defendantWaivesRightToCounsel?: boolean - @Allow() @IsOptional() @Field(() => DefendantPlea, { nullable: true }) @@ -87,4 +83,9 @@ export class UpdateDefendantInput { @IsOptional() @Field(() => String, { nullable: true }) readonly verdictViewDate?: string + + @Allow() + @IsOptional() + @Field(() => DefenderChoice, { nullable: true }) + readonly defenderChoice?: DefenderChoice } diff --git a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts index 50c1fed8da12..c52460c4125a 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts @@ -2,6 +2,7 @@ import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' import { DefendantPlea, + DefenderChoice, Gender, ServiceRequirement, } from '@island.is/judicial-system/types' @@ -9,6 +10,7 @@ import { registerEnumType(Gender, { name: 'Gender' }) registerEnumType(DefendantPlea, { name: 'DefendantPlea' }) registerEnumType(ServiceRequirement, { name: 'ServiceRequirement' }) +registerEnumType(DefenderChoice, { name: 'DefenderChoice' }) @ObjectType() export class Defendant { @@ -54,9 +56,6 @@ export class Defendant { @Field(() => String, { nullable: true }) readonly defenderPhoneNumber?: string - @Field(() => Boolean, { nullable: true }) - readonly defendantWaivesRightToCounsel?: boolean - @Field(() => DefendantPlea, { nullable: true }) readonly defendantPlea?: DefendantPlea @@ -68,4 +67,7 @@ export class Defendant { @Field(() => String, { nullable: true }) readonly verdictAppealDeadline?: string + + @Field(() => DefenderChoice, { nullable: true }) + readonly defenderChoice?: DefenderChoice } diff --git a/apps/judicial-system/api/src/app/modules/file/models/presignedPost.model.ts b/apps/judicial-system/api/src/app/modules/file/models/presignedPost.model.ts index b9bbc73f354e..c2d419bdee0e 100644 --- a/apps/judicial-system/api/src/app/modules/file/models/presignedPost.model.ts +++ b/apps/judicial-system/api/src/app/modules/file/models/presignedPost.model.ts @@ -9,4 +9,7 @@ export class PresignedPost { @Field(() => graphqlTypeJson) readonly fields!: { [key: string]: string } + + @Field(() => String) + readonly key!: string } diff --git a/apps/judicial-system/backend/migrations/20240522092213-update-file-keys.js b/apps/judicial-system/backend/migrations/20240522092213-update-file-keys.js new file mode 100644 index 000000000000..49b35862d68a --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240522092213-update-file-keys.js @@ -0,0 +1,67 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((transaction) => + Promise.all([ + queryInterface.sequelize.query( + `UPDATE case_file + SET key = SUBSTRING(key FROM 9) + WHERE key LIKE 'uploads/' || '%'; + UPDATE case_file + SET key = SUBSTRING(key FROM 23) + WHERE key LIKE 'indictments/completed/' || '%'; + UPDATE case_file + SET key = SUBSTRING(key FROM 13) + WHERE key LIKE 'indictments/' || '%';`, + { transaction }, + ), + queryInterface.addColumn( + 'case', + 'indictment_hash', + { type: Sequelize.STRING, allowNull: true }, + { transaction }, + ), + queryInterface.addColumn( + 'case_file', + 'hash', + { type: Sequelize.STRING, allowNull: true }, + { transaction }, + ), + ]), + ) + }, + + async down(queryInterface) { + return queryInterface.sequelize.transaction((transaction) => + Promise.all([ + queryInterface.sequelize.query( + `UPDATE case_file + SET key = 'uploads/' || key + WHERE key IS NOT NULL AND case_id IN ( + SELECT id + FROM "case" + WHERE type != 'INDICTMENT' + ); + UPDATE case_file + SET key = 'indictments/completed/' || key + WHERE key IS NOT NULL AND case_id IN ( + SELECT id + FROM "case" + WHERE type = 'INDICTMENT' AND state = 'COMPLETED' + ); + UPDATE case_file + SET key = 'indictments/' || key + WHERE key IS NOT NULL AND case_id IN ( + SELECT id + FROM "case" + WHERE type = 'INDICTMENT' AND state != 'COMPLETED' + );`, + { transaction }, + ), + queryInterface.removeColumn('case', 'indictment_hash', { transaction }), + queryInterface.removeColumn('case_file', 'hash', { transaction }), + ]), + ) + }, +} diff --git a/apps/judicial-system/backend/migrations/20240530132001-update-defendant.js b/apps/judicial-system/backend/migrations/20240530132001-update-defendant.js new file mode 100644 index 000000000000..2e08613fd81e --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240530132001-update-defendant.js @@ -0,0 +1,56 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface + .addColumn( + 'defendant', + 'defender_choice', + { + type: Sequelize.STRING, + allowNull: true, + }, + { transaction: t }, + ) + .then(() => + queryInterface.sequelize.query( + `UPDATE "defendant" SET defender_choice = 'WAIVE' WHERE defendant_waives_right_to_counsel = true;`, + { transaction: t }, + ), + ) + .then(() => + queryInterface.removeColumn( + 'defendant', + 'defendant_waives_right_to_counsel', + { transaction: t }, + ), + ), + ) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface + .addColumn( + 'defendant', + 'defendant_waives_right_to_counsel', + { + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false, + }, + { transaction: t }, + ) + .then(() => + queryInterface.sequelize.query( + `UPDATE "defendant" SET defendant_waives_right_to_counsel = true WHERE defender_choice = 'WAIVE';`, + { transaction: t }, + ), + ) + .then(() => + queryInterface.removeColumn('defendant', 'defender_choice', { + transaction: t, + }), + ), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts index 2f896a54141a..341797e8cfea 100644 --- a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts @@ -2,8 +2,8 @@ import { applyCase } from 'beygla' import { PDFDocument, rgb, StandardFonts } from 'pdf-lib' import { formatDate } from '@island.is/judicial-system/formatters' -import { IndictmentConfirmation } from '@island.is/judicial-system/types' +import { IndictmentConfirmation } from './indictmentPdf' import { drawTextWithEllipsisPDFKit } from './pdfHelpers' import { PDFKitCoatOfArms } from './PDFKitCoatOfArms' diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts b/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts index 31042ac3dba0..1e481e244591 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts @@ -46,6 +46,7 @@ export const makeProsecutor = (): User => { role: UserRole.PROSECUTOR, active: true, title: 'aðstoðarsaksóknari', + canConfirmIndictment: true, institution: { id: '', created: '', diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index b69fe76741ef..3807e29bbac5 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -32,4 +32,5 @@ export { export { getRequestPdfAsBuffer, getRequestPdfAsString } from './requestPdf' export { getRulingPdfAsBuffer, getRulingPdfAsString } from './rulingPdf' export { createCaseFilesRecord } from './caseFilesRecordPdf' -export { createIndictment } from './indictmentPdf' +export { createIndictment, IndictmentConfirmation } from './indictmentPdf' +export { createConfirmedIndictment } from './confirmedIndictmentPdf' diff --git a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts index 750b82bd7ce0..19716636f770 100644 --- a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts @@ -8,10 +8,6 @@ import { formatDate, lowercase, } from '@island.is/judicial-system/formatters' -import { - CaseState, - type IndictmentConfirmation, -} from '@island.is/judicial-system/types' import { nowFactory } from '../factories' import { indictment } from '../messages' @@ -56,6 +52,12 @@ const roman = (num: number) => { return str } +export interface IndictmentConfirmation { + actor: string + institution: string + date: Date +} + export const createIndictment = async ( theCase: Case, formatMessage: FormatMessage, @@ -81,7 +83,7 @@ export const createIndictment = async ( setTitle(doc, title) - if (theCase.state === CaseState.SUBMITTED && confirmation) { + if (confirmation) { addIndictmentConfirmation( doc, confirmation.actor, diff --git a/apps/judicial-system/backend/src/app/messages/courtUpload.ts b/apps/judicial-system/backend/src/app/messages/courtUpload.ts index c14ed09b7836..060c9cd8cb05 100644 --- a/apps/judicial-system/backend/src/app/messages/courtUpload.ts +++ b/apps/judicial-system/backend/src/app/messages/courtUpload.ts @@ -6,6 +6,11 @@ export const courtUpload = defineMessages({ defaultMessage: 'Krafa um {caseType} {date}', description: 'Notaður sem nafn á kröfuskjali í Auði.', }, + indictment: { + id: 'judicial.system.backend:court_upload.indictment', + defaultMessage: 'Ákæra', + description: 'Notaður sem nafn á ákæru í Auði.', + }, caseFilesRecord: { id: 'judicial.system.backend:court_upload.case_files_record', defaultMessage: 'Skjalaskrá {policeCaseNumber}', diff --git a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts index 3ffe4f13c044..1b781380af3b 100644 --- a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts +++ b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts @@ -2,10 +2,36 @@ import { S3 } from 'aws-sdk' import { Inject, Injectable } from '@nestjs/common' +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import type { ConfigType } from '@island.is/nest/config' +import { + CaseState, + CaseType, + isCompletedCase, + isIndictmentCase, +} from '@island.is/judicial-system/types' + import { awsS3ModuleConfig } from './awsS3.config' +const requestPrefix = 'uploads/' +const generatedPrefix = 'generated/' +const indictmentPrefix = 'indictments/' +const completedIndictmentPrefix = 'indictments/completed/' + +const formatConfirmedKey = (key: string) => + key.replace(/\/([^/]*)$/, '/confirmed/$1') +const formatS3RequestKey = (key: string) => `${requestPrefix}${key}` +const formatS3IndictmentKey = (key: string) => `${indictmentPrefix}${key}` +const formatS3CompletedIndictmentKey = (key: string) => + `${completedIndictmentPrefix}${key}` +const formatS3Key = (caseType: CaseType, caseState: CaseState, key: string) => + isIndictmentCase(caseType) + ? isCompletedCase(caseState) + ? formatS3CompletedIndictmentKey(key) + : formatS3IndictmentKey(key) + : formatS3RequestKey(key) + @Injectable() export class AwsS3Service { private readonly s3: S3 @@ -13,18 +39,24 @@ export class AwsS3Service { constructor( @Inject(awsS3ModuleConfig.KEY) private readonly config: ConfigType, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) { this.s3 = new S3({ region: this.config.region }) } - createPresignedPost(key: string, type: string): Promise { + createPresignedPost( + caseType: CaseType, + caseState: CaseState, + key: string, + type: string, + ): Promise { return new Promise((resolve, reject) => { this.s3.createPresignedPost( { Bucket: this.config.bucket, Expires: this.config.timeToLivePost, Fields: { - key, + key: formatS3Key(caseType, caseState, key), 'content-type': type, 'Content-Disposition': 'inline', }, @@ -40,7 +72,56 @@ export class AwsS3Service { }) } - getSignedUrl(key: string, timeToLive?: number): Promise { + private objectExistsInS3(key: string): Promise { + return this.s3 + .headObject({ + Bucket: this.config.bucket, + Key: key, + }) + .promise() + .then( + () => true, + () => { + // The error is either 404 Not Found or 403 Forbidden. + // Normally, we would check if the error is 404 Not Found. + // However, to avoid granting the service ListBucket permissions, + // we also allow 403 Forbidden. + return false + }, + ) + } + + private requestObjectExists(key: string): Promise { + return this.objectExistsInS3(formatS3RequestKey(key)) + } + + private async indictmentObjectExists( + caseSate: CaseState, + key: string, + ): Promise { + if (isCompletedCase(caseSate)) { + if (await this.objectExistsInS3(formatS3CompletedIndictmentKey(key))) { + return true + } + } + + return this.objectExistsInS3(formatS3IndictmentKey(key)) + } + + objectExists( + caseType: CaseType, + caseState: CaseState, + key: string, + ): Promise { + return isIndictmentCase(caseType) + ? this.indictmentObjectExists(caseState, key) + : this.requestObjectExists(key) + } + + private getSignedUrlFromS3( + key: string, + timeToLive?: number, + ): Promise { return new Promise((resolve, reject) => { this.s3.getSignedUrl( 'getObject', @@ -60,36 +141,89 @@ export class AwsS3Service { }) } - async deleteObject(key: string): Promise { - return this.s3 - .deleteObject({ - Bucket: this.config.bucket, - Key: key, - }) - .promise() - .then(() => true) + private getRequestSignedUrl( + key: string, + timeToLive?: number, + ): Promise { + return this.getSignedUrlFromS3(formatS3RequestKey(key), timeToLive) } - objectExists(key: string): Promise { - return this.s3 - .headObject({ - Bucket: this.config.bucket, - Key: key, - }) - .promise() - .then( - () => true, - () => { - // The error is either 404 Not Found or 403 Forbidden. - // Normally, we would check if the error is 404 Not Found. - // However, to avoid granting the service ListBucket permissions, - // we also allow 403 Forbidden. - return false - }, - ) + private async getIndictmentSignedUrl( + caseSate: CaseState, + key: string, + timeToLive?: number, + ): Promise { + if (isCompletedCase(caseSate)) { + const completedKey = formatS3CompletedIndictmentKey(key) + + if (await this.objectExistsInS3(completedKey)) { + return await this.getSignedUrlFromS3(completedKey, timeToLive) + } + } + + return this.getSignedUrlFromS3(formatS3IndictmentKey(key), timeToLive) + } + + getSignedUrl( + caseType: CaseType, + caseState: CaseState, + key?: string, + timeToLive?: number, + ): Promise { + if (!key) { + throw new Error('Key is required') + } + + return isIndictmentCase(caseType) + ? this.getIndictmentSignedUrl(caseState, key, timeToLive) + : this.getRequestSignedUrl(key, timeToLive) } - async getObject(key: string): Promise { + async getConfirmedSignedUrl( + caseType: CaseType, + caseState: CaseState, + key: string | undefined, + force: boolean, + confirmContent: (content: Buffer) => Promise, + timeToLive?: number, + ): Promise { + if (!key) { + throw new Error('Key is required') + } + + if (!isIndictmentCase(caseType)) { + throw new Error('Only indictment case objects can be confirmed') + } + + const confirmedKey = formatConfirmedKey(key) + + if ( + !force && + (await this.indictmentObjectExists(caseState, confirmedKey)) + ) { + return this.getIndictmentSignedUrl(caseState, confirmedKey, timeToLive) + } + + const confirmedContent = await this.getIndictmentObject( + caseState, + key, + ).then((content) => confirmContent(content)) + + if (!confirmedContent) { + return this.getIndictmentSignedUrl(caseState, key, timeToLive) + } + + return this.putConfirmedObject( + caseType, + caseState, + key, + confirmedContent, + ).then(() => + this.getIndictmentSignedUrl(caseState, confirmedKey, timeToLive), + ) + } + + private async getObjectFromS3(key: string): Promise { return this.s3 .getObject({ Bucket: this.config.bucket, @@ -99,7 +233,88 @@ export class AwsS3Service { .then((data) => data.Body as Buffer) } - async putObject(key: string, content: string): Promise { + private getRequestObject(key: string): Promise { + return this.getObjectFromS3(formatS3RequestKey(key)) + } + + private async getIndictmentObject( + caseSate: CaseState, + key: string, + ): Promise { + if (isCompletedCase(caseSate)) { + const completedKey = formatS3CompletedIndictmentKey(key) + + if (await this.objectExistsInS3(completedKey)) { + return await this.getObjectFromS3(completedKey) + } + } + + return this.getObjectFromS3(formatS3IndictmentKey(key)) + } + + async getObject( + caseType: CaseType, + caseState: CaseState, + key?: string, + ): Promise { + if (!key) { + throw new Error('Key is required') + } + + return isIndictmentCase(caseType) + ? this.getIndictmentObject(caseState, key) + : this.getRequestObject(key) + } + + getGeneratedObject(caseType: CaseType, key: string): Promise { + if (isIndictmentCase(caseType)) { + throw new Error('Only request case objects can be generated') + } + + return this.getObjectFromS3(`${generatedPrefix}${key}`) + } + + async getConfirmedObject( + caseType: CaseType, + caseState: CaseState, + key: string | undefined, + force: boolean, + confirmContent: (content: Buffer) => Promise, + ): Promise { + if (!key) { + throw new Error('Key is required') + } + + if (!isIndictmentCase(caseType)) { + throw new Error('Only indictment case objects can be confirmed') + } + + const confirmedKey = formatConfirmedKey(key) + + if ( + !force && + (await this.indictmentObjectExists(caseState, confirmedKey)) + ) { + return this.getIndictmentObject(caseState, confirmedKey) + } + + const content = await this.getIndictmentObject(caseState, key) + + const confirmedContent = await confirmContent(content) + + if (!confirmedContent) { + return this.getIndictmentObject(caseState, key) + } + + return this.putConfirmedObject( + caseType, + caseState, + key, + confirmedContent, + ).then(() => this.getIndictmentObject(caseState, confirmedKey)) + } + + private async putObjectToS3(key: string, content: string): Promise { return this.s3 .putObject({ Bucket: this.config.bucket, @@ -111,7 +326,95 @@ export class AwsS3Service { .then(() => key) } - async copyObject(key: string, newKey: string): Promise { + async putObject( + caseType: CaseType, + caseState: CaseState, + key: string, + content: string, + ): Promise { + return this.putObjectToS3(formatS3Key(caseType, caseState, key), content) + } + + putGeneratedObject( + caseType: CaseType, + key: string, + content: string, + ): Promise { + if (isIndictmentCase(caseType)) { + throw new Error('Only request case objects can be generated') + } + + return this.putObjectToS3(`${generatedPrefix}${key}`, content) + } + + putConfirmedObject( + caseType: CaseType, + caseState: CaseState, + key: string, + content: string, + ): Promise { + if (!isIndictmentCase(caseType)) { + throw new Error('Only indictment case objects can be confirmed') + } + + return this.putObject(caseType, caseState, formatConfirmedKey(key), content) + } + + private async deleteObjectFromS3(key: string): Promise { + return this.s3 + .deleteObject({ + Bucket: this.config.bucket, + Key: key, + }) + .promise() + .then(() => true) + .catch((reason) => { + // Tolerate failure, but log error + this.logger.error(`Failed to delete object ${key} from AWS S3`, { + reason, + }) + return false + }) + } + + private deleteRequestObject(key: string): Promise { + return this.deleteObjectFromS3(formatS3RequestKey(key)) + } + + private async deleteIndictmentObject( + caseSate: CaseState, + key: string, + ): Promise { + if (isCompletedCase(caseSate)) { + const completedKey = formatS3CompletedIndictmentKey(key) + + if (await this.objectExistsInS3(completedKey)) { + // No need to wait for the delete to finish + this.deleteObjectFromS3(formatConfirmedKey(completedKey)) + + return await this.deleteObjectFromS3(completedKey) + } + } + + const originalKey = formatS3IndictmentKey(key) + + // No need to wait for the delete to finish + this.deleteObjectFromS3(formatConfirmedKey(originalKey)) + + return this.deleteObjectFromS3(originalKey) + } + + async deleteObject( + caseType: CaseType, + caseState: CaseState, + key: string, + ): Promise { + return isIndictmentCase(caseType) + ? this.deleteIndictmentObject(caseState, key) + : this.deleteRequestObject(key) + } + + private async copyObject(key: string, newKey: string): Promise { return this.s3 .copyObject({ Bucket: this.config.bucket, @@ -121,4 +424,43 @@ export class AwsS3Service { .promise() .then(() => newKey) } + + async archiveObject( + caseType: CaseType, + caseState: CaseState, + key: string, + ): Promise { + if (!isIndictmentCase(caseType)) { + throw new Error('Only indictment case objects can be archived') + } + + if (!isCompletedCase(caseState)) { + throw new Error('Only completed indictment case objects can be archived') + } + + const oldKey = formatS3IndictmentKey(key) + const newKey = formatS3CompletedIndictmentKey(key) + + const oldConfirmedKey = formatConfirmedKey(oldKey) + + if (await this.objectExistsInS3(oldConfirmedKey)) { + const newConfirmedKey = formatConfirmedKey(newKey) + + if (!(await this.objectExistsInS3(newConfirmedKey))) { + await this.copyObject(oldConfirmedKey, newConfirmedKey) + } + + // No need to wait for the delete to finish + this.deleteObjectFromS3(oldConfirmedKey) + } + + if (!(await this.objectExistsInS3(newKey))) { + await this.copyObject(oldKey, newKey) + } + + // No need to wait for the delete to finish + this.deleteObjectFromS3(oldKey) + + return newKey + } } diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index a06ffadef641..d055e66339fb 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -96,7 +96,6 @@ import { } from './guards/rolesRules' import { CaseInterceptor } from './interceptors/case.interceptor' import { CaseListInterceptor } from './interceptors/caseList.interceptor' -import { TransitionInterceptor } from './interceptors/transition.interceptor' import { Case } from './models/case.model' import { SignatureConfirmationResponse } from './models/signatureConfirmation.response' import { transitionCase } from './state/case.state' @@ -260,7 +259,6 @@ export class CaseController { } @UseGuards(JwtAuthGuard, CaseExistsGuard, RolesGuard, CaseWriteGuard) - @UseInterceptors(TransitionInterceptor) @RolesRules( prosecutorTransitionRule, prosecutorRepresentativeTransitionRule, @@ -304,9 +302,8 @@ export class CaseController { `User ${user.id} does not have permission to confirm indictments`, ) } - if (theCase.indictmentDeniedExplanation) { - update.indictmentDeniedExplanation = '' - } + + update.indictmentDeniedExplanation = null } break case CaseTransition.ACCEPT: @@ -339,7 +336,6 @@ export class CaseController { ), } } - break case CaseTransition.REOPEN: update.rulingDate = null @@ -399,12 +395,11 @@ export class CaseController { } break case CaseTransition.ASK_FOR_CONFIRMATION: - if (theCase.indictmentReturnedExplanation) { - update.indictmentReturnedExplanation = '' - } + update.indictmentReturnedExplanation = null break case CaseTransition.RETURN_INDICTMENT: - update.courtCaseNumber = '' + update.courtCaseNumber = null + update.indictmentHash = null break case CaseTransition.REDISTRIBUTE: update.judgeId = null diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index e01cb4ae6e1e..408ad4b46bd6 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -41,6 +41,7 @@ import { isCompletedCase, isIndictmentCase, isRestrictionCase, + isTrafficViolationCase, NotificationType, prosecutorCanSelectDefenderForInvestigationCase, UserRole, @@ -55,7 +56,7 @@ import { AwsS3Service } from '../aws-s3' import { CourtService } from '../court' import { Defendant, DefendantService } from '../defendant' import { CaseEvent, EventService } from '../event' -import { EventLog } from '../event-log' +import { EventLog, EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' import { IndictmentCount } from '../indictment-count' import { Institution } from '../institution' @@ -104,7 +105,6 @@ export interface UpdateCase | 'caseFilesComments' | 'prosecutorId' | 'sharedWithProsecutorsOfficeId' - | 'courtCaseNumber' | 'sessionArrangements' | 'courtLocation' | 'courtStartDate' @@ -153,8 +153,6 @@ export interface UpdateCase | 'appealValidToDate' | 'isAppealCustodyIsolation' | 'appealIsolationToDate' - | 'indictmentDeniedExplanation' - | 'indictmentReturnedExplanation' | 'indictmentRulingDecision' | 'indictmentReviewerId' > { @@ -164,6 +162,7 @@ export interface UpdateCase defendantWaivesRightToCounsel?: boolean rulingDate?: Date | null rulingSignatureDate?: Date | null + courtCaseNumber?: string | null courtRecordSignatoryId?: string | null courtRecordSignatureDate?: Date | null parentCaseId?: string | null @@ -171,6 +170,9 @@ export interface UpdateCase courtDate?: UpdateDateLog | null postponedIndefinitelyExplanation?: string | null judgeId?: string | null + indictmentReturnedExplanation?: string | null + indictmentDeniedExplanation?: string | null + indictmentHash?: string | null } type DateLogKeys = keyof Pick @@ -243,6 +245,11 @@ export const include: Includeable[] = [ as: 'appealJudge3', include: [{ model: Institution, as: 'institution' }], }, + { + model: User, + as: 'indictmentReviewer', + include: [{ model: Institution, as: 'institution' }], + }, { model: Case, as: 'parentCase', @@ -271,6 +278,7 @@ export const include: Includeable[] = [ as: 'eventLogs', required: false, where: { eventType: { [Op.in]: eventTypes } }, + order: [['created', 'ASC']], separate: true, }, { @@ -286,11 +294,6 @@ export const include: Includeable[] = [ where: { commentType: { [Op.in]: commentTypes } }, }, { model: Notification, as: 'notifications' }, - { - model: User, - as: 'indictmentReviewer', - include: [{ model: Institution, as: 'institution' }], - }, ] export const order: OrderItem[] = [ @@ -364,6 +367,7 @@ export class CaseService { private readonly signingService: SigningService, private readonly intlService: IntlService, private readonly eventService: EventService, + private readonly eventLogService: EventLogService, private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -388,7 +392,7 @@ export class CaseService { pdf: string, ): Promise { return this.awsS3Service - .putObject(`generated/${theCase.id}/ruling.pdf`, pdf) + .putGeneratedObject(theCase.type, `${theCase.id}/ruling.pdf`, pdf) .then(() => true) .catch((reason) => { this.logger.error( @@ -405,7 +409,7 @@ export class CaseService { pdf: string, ): Promise { return this.awsS3Service - .putObject(`generated/${theCase.id}/courtRecord.pdf`, pdf) + .putGeneratedObject(theCase.type, `${theCase.id}/courtRecord.pdf`, pdf) .then(() => true) .catch((reason) => { this.logger.error( @@ -465,7 +469,11 @@ export class CaseService { ) { for (const caseFile of theCase.caseFiles) { if (caseFile.policeCaseNumber === oldPoliceCaseNumbers[i]) { - await this.fileService.deleteCaseFile(caseFile, transaction) + await this.fileService.deleteCaseFile( + theCase, + caseFile, + transaction, + ) } } @@ -642,6 +650,19 @@ export class CaseService { elementId: policeCaseNumber, })) + const caseFilesCategories = isTrafficViolationCase(theCase) + ? [ + CaseFileCategory.COVER_LETTER, + CaseFileCategory.CRIMINAL_RECORD, + CaseFileCategory.COST_BREAKDOWN, + ] + : [ + CaseFileCategory.COVER_LETTER, + CaseFileCategory.INDICTMENT, + CaseFileCategory.CRIMINAL_RECORD, + CaseFileCategory.COST_BREAKDOWN, + ] + const deliverCaseFileToCourtMessages = theCase.caseFiles ?.filter( @@ -649,12 +670,7 @@ export class CaseService { caseFile.state === CaseFileState.STORED_IN_RVG && caseFile.key && ((caseFile.category && - [ - CaseFileCategory.COVER_LETTER, - CaseFileCategory.INDICTMENT, - CaseFileCategory.CRIMINAL_RECORD, - CaseFileCategory.COST_BREAKDOWN, - ].includes(caseFile.category)) || + caseFilesCategories.includes(caseFile.category)) || (caseFile.category === CaseFileCategory.CASE_FILE && !caseFile.policeCaseNumber)), ) @@ -665,12 +681,20 @@ export class CaseService { elementId: caseFile.id, })) ?? [] - return this.messageService.sendMessagesToQueue( - this.getDeliverProsecutorToCourtMessages(theCase, user) - .concat(this.getDeliverDefendantToCourtMessages(theCase, user)) - .concat(deliverCaseFilesRecordToCourtMessages) - .concat(deliverCaseFileToCourtMessages), - ) + const messages = this.getDeliverProsecutorToCourtMessages(theCase, user) + .concat(this.getDeliverDefendantToCourtMessages(theCase, user)) + .concat(deliverCaseFilesRecordToCourtMessages) + .concat(deliverCaseFileToCourtMessages) + + if (isTrafficViolationCase(theCase)) { + messages.push({ + type: MessageType.DELIVERY_TO_COURT_INDICTMENT, + user, + caseId: theCase.id, + }) + } + + return this.messageService.sendMessagesToQueue(messages) } private addMessagesForDefenderEmailChangeToQueue( @@ -1343,6 +1367,29 @@ export class CaseService { } } + handleEventLogs( + theCase: Case, + update: UpdateCase, + user: TUser, + transaction: Transaction, + ) { + if ( + isIndictmentCase(theCase.type) && + update.state === CaseState.SUBMITTED && + theCase.state === CaseState.WAITING_FOR_CONFIRMATION + ) { + return this.eventLogService.create( + { + eventType: EventType.INDICTMENT_CONFIRMED, + caseId: theCase.id, + nationalId: user.nationalId, + userRole: user.role, + }, + transaction, + ) + } + } + async update( theCase: Case, update: UpdateCase, @@ -1351,6 +1398,8 @@ export class CaseService { ): Promise { const receivingCase = update.courtCaseNumber && theCase.state === CaseState.SUBMITTED + const returningIndictmentCase = + update.state === CaseState.DRAFT && theCase.state === CaseState.RECEIVED return this.sequelize .transaction(async (transaction) => { @@ -1365,6 +1414,7 @@ export class CaseService { await this.handleDateUpdates(theCase, update, transaction) await this.handleCommentUpdates(theCase, update, transaction) + await this.handleEventLogs(theCase, update, user, transaction) if (Object.keys(update).length === 0) { return @@ -1401,6 +1451,13 @@ export class CaseService { ) { await this.fileService.resetCaseFileStates(theCase.id, transaction) } + + if (returningIndictmentCase) { + await this.fileService.resetIndictmentCaseFileHashes( + theCase.id, + transaction, + ) + } }) .then(async () => { const updatedCase = await this.findById(theCase.id, true) diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts index f4c2d84aaa4c..838275876e6d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts @@ -263,7 +263,6 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { } export const getCasesQueryFilter = (user: User): WhereOptions => { - // TODO: Convert to switch if (isProsecutionUser(user)) { return getProsecutionUserCasesQueryFilter(user) } diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts index af0efc03b223..940d763221d6 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts @@ -58,6 +58,7 @@ export class CaseListInterceptor implements NestInterceptor { )?.comment, indictmentReviewer: theCase.indictmentReviewer, indictmentReviewDecision: theCase.indictmentReviewDecision, + indictmentRulingDecision: theCase.indictmentRulingDecision, } }), ), diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/transition.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/transition.interceptor.ts deleted file mode 100644 index d95e51de802e..000000000000 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/transition.interceptor.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' - -import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, -} from '@nestjs/common' - -import { - CaseFileCategory, - CaseState, - CaseTransition, - EventType, - isIndictmentCase, - isTrafficViolationCase, - User, -} from '@island.is/judicial-system/types' - -import { nowFactory } from '../../../factories' -import { formatConfirmedIndictmentKey } from '../../../formatters/formatters' -import { AwsS3Service } from '../../aws-s3' -import { EventLogService } from '../../event-log' -import { TransitionCaseDto } from '../dto/transitionCase.dto' -import { Case } from '../models/case.model' -import { PDFService } from '../pdf.service' - -@Injectable() -export class TransitionInterceptor implements NestInterceptor { - constructor( - private readonly eventLogService: EventLogService, - private readonly awsService: AwsS3Service, - private readonly pdfService: PDFService, - ) {} - - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { - const request = context.switchToHttp().getRequest() - const theCase: Case = request.case - const dto: TransitionCaseDto = request.body - const user: User = request.user - - if ( - isIndictmentCase(theCase.type) && - !isTrafficViolationCase(theCase.indictmentSubtypes, theCase.type) && - theCase.state === CaseState.WAITING_FOR_CONFIRMATION && - dto.transition === CaseTransition.SUBMIT - ) { - for (const indictment of theCase.caseFiles?.filter( - (cf) => cf.category === CaseFileCategory.INDICTMENT && cf.key, - ) ?? []) { - // Get indictment PDF from S3 - const file = await this.awsService.getObject(indictment.key ?? '') - - // Create a stamped indictment PDF - const confirmedIndictment = - await this.pdfService.getConfirmedIndictmentPdf( - { - actor: user.name, - institution: user.institution?.name ?? '', - date: nowFactory(), - }, - file, - ) - - // Save the PDF to S3 - await this.awsService.putObject( - formatConfirmedIndictmentKey(indictment.key), - confirmedIndictment.toString('binary'), - ) - } - } - - return next.handle().pipe( - map((data: Case) => { - if (isIndictmentCase(data.type) && data.state === CaseState.SUBMITTED) { - this.eventLogService.create({ - eventType: EventType.INDICTMENT_CONFIRMED, - caseId: data.id, - nationalId: user.nationalId, - userRole: user.role, - }) - } - - return data - }), - ) - } -} diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts index 585a3e7c04f4..bc47e8277231 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts @@ -80,9 +80,27 @@ export class InternalCaseController { @Body() internalCasesDto: InternalCasesDto, ): Promise { this.logger.debug('Getting all indictment cases') - const nationalId = formatNationalId(internalCasesDto.nationalId) - return this.internalCaseService.getIndictmentCases(nationalId) + return this.internalCaseService.getIndictmentCases( + internalCasesDto.nationalId, + ) + } + + @Post('cases/indictment/:caseId') + @ApiOkResponse({ + type: Case, + description: 'Gets indictment case by id', + }) + getIndictmentCase( + @Param('caseId') caseId: string, + @Body() internalCasesDto: InternalCasesDto, + ): Promise { + this.logger.debug(`Getting indictment case ${caseId}`) + + return this.internalCaseService.getIndictmentCase( + caseId, + internalCasesDto.nationalId, + ) } @UseGuards(CaseExistsGuard) @@ -106,6 +124,27 @@ export class InternalCaseController { ) } + @UseGuards(CaseExistsGuard, new CaseTypeGuard(indictmentCases)) + @Post( + `case/:caseId/${messageEndpoint[MessageType.DELIVERY_TO_COURT_INDICTMENT]}`, + ) + @ApiOkResponse({ + type: DeliverResponse, + description: 'Delivers an indictment to court', + }) + deliverIndictmentToCourt( + @Param('caseId') caseId: string, + @CurrentCase() theCase: Case, + @Body() deliverDto: DeliverDto, + ): Promise { + this.logger.debug(`Delivering the indictment for case ${caseId} to court`) + + return this.internalCaseService.deliverIndictmentToCourt( + theCase, + deliverDto.user, + ) + } + @UseGuards(CaseExistsGuard, new CaseTypeGuard(indictmentCases)) @Post( `case/:caseId/${ diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index 690b48e5e69e..028c0a158b3f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -10,6 +10,7 @@ import { Inject, Injectable, InternalServerErrorException, + NotFoundException, } from '@nestjs/common' import { InjectConnection, InjectModel } from '@nestjs/sequelize' @@ -17,18 +18,19 @@ import { FormatMessage, IntlService } from '@island.is/cms-translations' import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import type { ConfigType } from '@island.is/nest/config' -import { formatCaseType } from '@island.is/judicial-system/formatters' +import { + formatCaseType, + formatNationalId, +} from '@island.is/judicial-system/formatters' import { CaseFileCategory, CaseOrigin, CaseState, CaseType, - EventType, - type IndictmentConfirmation, - isCompletedCase, isIndictmentCase, isProsecutionUser, isRestrictionCase, + isTrafficViolationCase, NotificationType, restrictionCases, type User as TUser, @@ -37,8 +39,6 @@ import { import { nowFactory, uuidFactory } from '../../factories' import { - createCaseFilesRecord, - createIndictment, getCourtRecordPdfAsBuffer, getCourtRecordPdfAsString, getCustodyNoticePdfAsString, @@ -53,8 +53,9 @@ import { CaseEvent, EventService } from '../event' import { EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' import { IndictmentCount, IndictmentCountService } from '../indictment-count' -import { CourtDocumentType, PoliceService } from '../police' -import { UserService } from '../user' +import { Institution } from '../institution' +import { PoliceDocument, PoliceDocumentType, PoliceService } from '../police' +import { User, UserService } from '../user' import { InternalCreateCaseDto } from './dto/internalCreateCase.dto' import { archiveFilter } from './filters/case.archiveFilter' import { ArchiveResponse } from './models/archive.response' @@ -63,6 +64,7 @@ import { CaseArchive } from './models/caseArchive.model' import { DateLog } from './models/dateLog.model' import { DeliverResponse } from './models/deliver.response' import { caseModuleConfig } from './case.config' +import { PDFService } from './pdf.service' const caseEncryptionProperties: (keyof Case)[] = [ 'description', @@ -160,6 +162,7 @@ export class InternalCaseService { private readonly defendantService: DefendantService, @Inject(forwardRef(() => EventLogService)) private readonly eventLogService: EventLogService, + private readonly pdfService: PDFService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -284,135 +287,11 @@ export class InternalCaseService { }) } - private async getCaseFilesRecordPdf( - theCase: Case, - policeCaseNumber: string, - ): Promise { - if (isCompletedCase(theCase.state)) { - try { - return await this.awsS3Service.getObject( - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - } catch { - // Ignore the error and try the original key - } - } - - try { - return await this.awsS3Service.getObject( - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - } catch { - // Ignore the error and generate the pdf - } - - const caseFiles = theCase.caseFiles - ?.filter( - (caseFile) => - caseFile.policeCaseNumber === policeCaseNumber && - caseFile.category === CaseFileCategory.CASE_FILE && - caseFile.type === 'application/pdf' && - caseFile.key && - caseFile.chapter !== null && - caseFile.orderWithinChapter !== null, - ) - ?.sort( - (caseFile1, caseFile2) => - (caseFile1.chapter ?? 0) - (caseFile2.chapter ?? 0) || - (caseFile1.orderWithinChapter ?? 0) - - (caseFile2.orderWithinChapter ?? 0), - ) - ?.map((caseFile) => async () => { - const buffer = await this.awsS3Service - .getObject(caseFile.key ?? '') - .catch((reason) => { - // Tolerate failure, but log error - this.logger.error( - `Unable to get file ${caseFile.id} of case ${theCase.id} from AWS S3`, - { reason }, - ) - }) - - return { - chapter: caseFile.chapter as number, - date: caseFile.displayDate ?? caseFile.created, - name: caseFile.userGeneratedFilename ?? caseFile.name, - buffer: buffer ?? undefined, - } - }) - - const pdf = await createCaseFilesRecord( - theCase, - policeCaseNumber, - caseFiles ?? [], - this.formatMessage, - ) - - await this.awsS3Service - .putObject( - `indictments/${isCompletedCase(theCase.state) ? 'completed/' : ''}${ - theCase.id - }/${policeCaseNumber}/caseFilesRecord.pdf`, - pdf.toString('binary'), - ) - .catch((reason) => { - this.logger.error( - `Failed to upload case files record pdf to AWS S3 for case ${theCase.id} and police case ${policeCaseNumber}`, - { reason }, - ) - }) - - return pdf - } - - private async throttleUploadCaseFilesRecordPdfToCourt( - theCase: Case, - policeCaseNumber: string, - user: TUser, - ): Promise { - // Serialize all case files record pdf deliveries in this process - await this.throttle.catch((reason) => { - this.logger.info('Previous case files record pdf delivery failed', { - reason, - }) - }) - - await this.refreshFormatMessage() - - return this.getCaseFilesRecordPdf(theCase, policeCaseNumber) - .then((pdf) => { - const fileName = this.formatMessage(courtUpload.caseFilesRecord, { - policeCaseNumber, - }) - - return this.courtService.createDocument( - user, - theCase.id, - theCase.courtId, - theCase.courtCaseNumber, - CourtDocumentFolder.CASE_DOCUMENTS, - fileName, - `${fileName}.pdf`, - 'application/pdf', - pdf, - ) - }) - .then(() => { - return true - }) - .catch((error) => { - // Tolerate failure, but log error - this.logger.warn( - `Failed to upload case files record pdf to court for case ${theCase.id}`, - { error }, - ) - - return false - }) - } - private getSignedRulingPdf(theCase: Case) { - return this.awsS3Service.getObject(`generated/${theCase.id}/ruling.pdf`) + return this.awsS3Service.getGeneratedObject( + theCase.type, + `${theCase.id}/ruling.pdf`, + ) } private async deliverSignedRulingPdfToCourt( @@ -628,20 +507,77 @@ export class InternalCaseService { }) } + async deliverIndictmentToCourt( + theCase: Case, + user: TUser, + ): Promise { + return this.pdfService + .getIndictmentPdf(theCase) + .then(async (pdf) => { + await this.refreshFormatMessage() + + const fileName = this.formatMessage(courtUpload.indictment) + + return this.courtService.createDocument( + user, + theCase.id, + theCase.courtId, + theCase.courtCaseNumber, + CourtDocumentFolder.INDICTMENT_DOCUMENTS, + fileName, + `${fileName}.pdf`, + 'application/pdf', + pdf, + ) + }) + .then(() => ({ delivered: true })) + .catch((reason) => { + // Tolerate failure, but log error + this.logger.warn( + `Failed to upload indictment pdf to court for case ${theCase.id}`, + { reason }, + ) + + return { delivered: false } + }) + } + async deliverCaseFilesRecordToCourt( theCase: Case, policeCaseNumber: string, user: TUser, ): Promise { - this.throttle = this.throttleUploadCaseFilesRecordPdfToCourt( - theCase, - policeCaseNumber, - user, - ) + return this.pdfService + .getCaseFilesRecordPdf(theCase, policeCaseNumber) + .then(async (pdf) => { + await this.refreshFormatMessage() - const delivered = await this.throttle + const fileName = this.formatMessage(courtUpload.caseFilesRecord, { + policeCaseNumber, + }) - return { delivered } + return this.courtService.createDocument( + user, + theCase.id, + theCase.courtId, + theCase.courtCaseNumber, + CourtDocumentFolder.CASE_DOCUMENTS, + fileName, + `${fileName}.pdf`, + 'application/pdf', + pdf, + ) + }) + .then(() => ({ delivered: true })) + .catch((reason) => { + // Tolerate failure, but log reason + this.logger.warn( + `Failed to upload case files record pdf to court for case ${theCase.id}`, + { reason }, + ) + + return { delivered: false } + }) } async archiveCaseFilesRecord( @@ -649,26 +585,12 @@ export class InternalCaseService { policeCaseNumber: string, ): Promise { return this.awsS3Service - .copyObject( - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + .archiveObject( + theCase.type, + theCase.state, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, ) - .then(() => { - // Fire and forget, no need to wait for the result - this.awsS3Service - .deleteObject( - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - .catch((reason) => { - // Tolerate failure, but log what happened - this.logger.error( - `Could not delete case files record for case ${theCase.id} and police case ${policeCaseNumber} from AWS S3`, - { reason }, - ) - }) - - return { delivered: true } - }) + .then(() => ({ delivered: true })) .catch((reason) => { this.logger.error( `Failed to archive case files record for case ${theCase.id} and police case ${policeCaseNumber}`, @@ -685,9 +607,9 @@ export class InternalCaseService { ): Promise { await this.refreshFormatMessage() - const delivered = await this.upploadRequestPdfToCourt(theCase, user) - - return { delivered } + return this.upploadRequestPdfToCourt(theCase, user).then((delivered) => ({ + delivered, + })) } async deliverCourtRecordToCourt( @@ -696,9 +618,9 @@ export class InternalCaseService { ): Promise { await this.refreshFormatMessage() - const delivered = await this.uploadCourtRecordPdfToCourt(theCase, user) - - return { delivered } + return this.uploadCourtRecordPdfToCourt(theCase, user).then( + (delivered) => ({ delivered }), + ) } async deliverSignedRulingToCourt( @@ -707,9 +629,9 @@ export class InternalCaseService { ): Promise { await this.refreshFormatMessage() - const delivered = await this.deliverSignedRulingPdfToCourt(theCase, user) - - return { delivered } + return this.deliverSignedRulingPdfToCourt(theCase, user).then( + (delivered) => ({ delivered }), + ) } async deliverCaseConclusionToCourt( @@ -830,7 +752,7 @@ export class InternalCaseService { private async deliverCaseToPoliceWithFiles( theCase: Case, user: TUser, - courtDocuments: { type: CourtDocumentType; courtDocument: string }[], + courtDocuments: PoliceDocument[], ): Promise { const originalAncestor = await this.findOriginalAncestor(theCase) @@ -872,13 +794,13 @@ export class InternalCaseService { .then(async () => { const courtDocuments = [ { - type: CourtDocumentType.RVKR, + type: PoliceDocumentType.RVKR, courtDocument: Base64.btoa( await getRequestPdfAsString(theCase, this.formatMessage), ), }, { - type: CourtDocumentType.RVTB, + type: PoliceDocumentType.RVTB, courtDocument: Base64.btoa( await getCourtRecordPdfAsString(theCase, this.formatMessage), ), @@ -888,7 +810,7 @@ export class InternalCaseService { ) && theCase.state === CaseState.ACCEPTED ? [ { - type: CourtDocumentType.RVVI, + type: PoliceDocumentType.RVVI, courtDocument: Base64.btoa( await getCustodyNoticePdfAsString( theCase, @@ -929,13 +851,18 @@ export class InternalCaseService { caseFile.key, ) .map(async (caseFile) => { - const file = await this.awsS3Service.getObject(caseFile.key ?? '') + // TODO: Tolerate failure, but log error + const file = await this.awsS3Service.getObject( + theCase.type, + theCase.state, + caseFile.key, + ) return { type: caseFile.category === CaseFileCategory.COURT_RECORD - ? CourtDocumentType.RVTB - : CourtDocumentType.RVDO, + ? PoliceDocumentType.RVTB + : PoliceDocumentType.RVDO, courtDocument: Base64.btoa(file.toString('binary')), } }) ?? [], @@ -959,91 +886,70 @@ export class InternalCaseService { theCase: Case, user: TUser, ): Promise { - const delivered = await Promise.all( - theCase.caseFiles - ?.filter( - (caseFile) => - caseFile.category === CaseFileCategory.INDICTMENT && caseFile.key, - ) - .map(async (caseFile) => { - const file = await this.awsS3Service.getObject(caseFile.key ?? '') - - return { - type: CourtDocumentType.RVAS, - courtDocument: Base64.btoa(file.toString('binary')), - } - }) ?? [], - ) - .then(async (indictmentDocuments) => { - if (indictmentDocuments.length === 0) { - let confirmation: IndictmentConfirmation = undefined - const confirmationEvent = - await this.eventLogService.findEventTypeByCaseId( - EventType.INDICTMENT_CONFIRMED, - theCase.id, - ) - - if (confirmationEvent && confirmationEvent.nationalId) { - const actor = await this.userService.findByNationalId( - confirmationEvent.nationalId, - ) - - confirmation = { - actor: actor.name, - institution: actor.institution?.name ?? '', - date: confirmationEvent.created, - } - } - - const file = await this.refreshFormatMessage().then(async () => - createIndictment(theCase, this.formatMessage, confirmation), - ) + try { + let policeDocuments: PoliceDocument[] - indictmentDocuments.push({ - type: CourtDocumentType.RVAS, - courtDocument: Base64.btoa(file.toString('binary')), - }) - } + if (isTrafficViolationCase(theCase)) { + const file = await this.pdfService.getIndictmentPdf(theCase) - return this.deliverCaseToPoliceWithFiles( - theCase, - user, - indictmentDocuments, - ) - }) - .catch((reason) => { - // Tolerate failure, but log error - this.logger.error( - `Failed to deliver indictment for case ${theCase.id} to police`, + policeDocuments = [ { - reason, + type: PoliceDocumentType.RVAS, + courtDocument: Base64.btoa(file.toString('binary')), }, + ] + } else { + policeDocuments = await Promise.all( + theCase.caseFiles + ?.filter( + (caseFile) => + caseFile.category === CaseFileCategory.INDICTMENT && + caseFile.key, + ) + .map(async (caseFile) => { + // TODO: Tolerate failure, but log error + const file = await this.fileService.getCaseFileFromS3( + theCase, + caseFile, + ) + + return { + type: PoliceDocumentType.RVAS, + courtDocument: Base64.btoa(file.toString('binary')), + } + }) ?? [], ) + } - return false - }) + const delivered = await this.deliverCaseToPoliceWithFiles( + theCase, + user, + policeDocuments, + ) - return { delivered } + return { delivered } + } catch (error) { + // Tolerate failure, but log error + this.logger.error( + `Failed to deliver indictment for case ${theCase.id} to police`, + { error }, + ) + + return { delivered: false } + } } - private async throttleUploadCaseFilesRecordPdfToPolice( + async deliverCaseFilesRecordToPolice( theCase: Case, policeCaseNumber: string, user: TUser, - ): Promise { - // Serialize all case files record pdf deliveries in this process - await this.throttle.catch((reason) => { - this.logger.info('Previous case files record pdf delivery failed', { - reason, - }) - }) - - return this.refreshFormatMessage() - .then(() => this.getCaseFilesRecordPdf(theCase, policeCaseNumber)) + ): Promise { + const delivered = await this.pdfService + .getCaseFilesRecordPdf(theCase, policeCaseNumber) .then((pdf) => this.deliverCaseToPoliceWithFiles(theCase, user, [ { - type: CourtDocumentType.RVMG, + type: PoliceDocumentType.RVMG, courtDocument: Base64.btoa(pdf.toString('binary')), }, ]), @@ -1057,20 +963,6 @@ export class InternalCaseService { return false }) - } - - async deliverCaseFilesRecordToPolice( - theCase: Case, - policeCaseNumber: string, - user: TUser, - ): Promise { - this.throttle = this.throttleUploadCaseFilesRecordPdfToPolice( - theCase, - policeCaseNumber, - user, - ) - - const delivered = await this.throttle return { delivered } } @@ -1083,7 +975,7 @@ export class InternalCaseService { .then((pdf) => this.deliverCaseToPoliceWithFiles(theCase, user, [ { - type: CourtDocumentType.RVUR, + type: PoliceDocumentType.RVUR, courtDocument: Base64.btoa(pdf.toString('binary')), }, ]), @@ -1109,10 +1001,15 @@ export class InternalCaseService { theCase.caseFiles ?.filter((file) => file.category === CaseFileCategory.APPEAL_RULING) .map(async (caseFile) => { - const file = await this.awsS3Service.getObject(caseFile.key ?? '') + // TODO: Tolerate failure, but log error + const file = await this.awsS3Service.getObject( + theCase.type, + theCase.state, + caseFile.key, + ) return { - type: CourtDocumentType.RVUL, + type: PoliceDocumentType.RVUL, courtDocument: Base64.btoa(file.toString('binary')), } }) ?? [], @@ -1154,6 +1051,8 @@ export class InternalCaseService { } async getIndictmentCases(nationalId: string): Promise { + const formattedNationalId = formatNationalId(nationalId) + return this.caseModel.findAll({ include: [ { model: Defendant, as: 'defendants' }, @@ -1163,8 +1062,45 @@ export class InternalCaseService { attributes: ['id', 'courtCaseNumber', 'type', 'state'], where: { type: CaseType.INDICTMENT, - '$defendants.national_id$': nationalId, + [Op.or]: [ + { '$defendants.national_id$': nationalId }, + { '$defendants.national_id$': formattedNationalId }, + ], }, }) } + + async getIndictmentCase( + caseId: string, + nationalId: string, + ): Promise { + // The national id could be without a hyphen or with a hyphen so we need to + // search for both + const formattedNationalId = formatNationalId(nationalId) + + const caseById = await this.caseModel.findOne({ + include: [ + { model: Defendant, as: 'defendants' }, + { model: Institution, as: 'court' }, + { model: Institution, as: 'prosecutorsOffice' }, + { model: User, as: 'judge' }, + { model: User, as: 'prosecutor' }, + ], + attributes: ['courtCaseNumber'], + where: { + type: CaseType.INDICTMENT, + id: caseId, + [Op.or]: [ + { '$defendants.national_id$': nationalId }, + { '$defendants.national_id$': formattedNationalId }, + ], + }, + }) + + if (!caseById) { + throw new NotFoundException(`Case ${caseId} not found`) + } + + return caseById + } } diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index a0e44ad60792..bf2410586094 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -26,6 +26,7 @@ import { CaseState, CommentType, DateType, + EventType, NotificationType, UserRole, } from '@island.is/judicial-system/types' @@ -33,6 +34,7 @@ import { import { nowFactory, uuidFactory } from '../../factories' import { AwsS3Service } from '../aws-s3' import { Defendant, DefendantService } from '../defendant' +import { EventLog } from '../event-log' import { CaseFile, defenderCaseFileCategoriesForRestrictionAndInvestigationCases, @@ -97,6 +99,8 @@ export const attributes: (keyof Case)[] = [ 'appealRulingModifiedHistory', 'requestAppealRulingNotToBePublished', 'prosecutorsOfficeId', + 'indictmentRulingDecision', + 'indictmentHash', ] export interface LimitedAccessUpdateCase @@ -109,6 +113,7 @@ export interface LimitedAccessUpdateCase | 'appealRulingDecision' > {} +const eventTypes = Object.values(EventType) const dateTypes = Object.values(DateType) const commentTypes = Object.values(CommentType) @@ -184,6 +189,14 @@ export const include: Includeable[] = [ ], }, }, + { + model: EventLog, + as: 'eventLogs', + required: false, + where: { eventType: { [Op.in]: eventTypes } }, + order: [['created', 'ASC']], + separate: true, + }, { model: DateLog, as: 'dateLogs', @@ -377,9 +390,7 @@ export class LimitedAccessCaseService { }) } - private zipFiles( - files: Array<{ data: Buffer; name: string }>, - ): Promise { + private zipFiles(files: { data: Buffer; name: string }[]): Promise { return new Promise((resolve, reject) => { const buffs: Buffer[] = [] const converter = new Writable() @@ -410,7 +421,7 @@ export class LimitedAccessCaseService { } async getAllFilesZip(theCase: Case, user: TUser): Promise { - const filesToZip: Array<{ data: Buffer; name: string }> = [] + const filesToZip: { data: Buffer; name: string }[] = [] const caseFilesByCategory = theCase.caseFiles?.filter( @@ -422,9 +433,10 @@ export class LimitedAccessCaseService { ), ) ?? [] + // TODO: speed this up by fetching all files in parallel for (const file of caseFilesByCategory) { await this.awsS3Service - .getObject(file.key ?? '') + .getObject(theCase.type, theCase.state, file.key) .then((content) => filesToZip.push({ data: content, name: file.name })) .catch((reason) => // Tolerate failure, but log what happened diff --git a/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts b/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts index 81117d9db4cf..fb6261333a6a 100644 --- a/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts +++ b/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts @@ -1013,4 +1013,12 @@ export class Case extends Model { }) @ApiPropertyOptional({ enum: IndictmentCaseReviewDecision }) indictmentReviewDecision?: IndictmentCaseReviewDecision + + /********** + * The md5 hash of the confirmed generated indictment + * Only used for traffic violation cases + **********/ + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + indictmentHash?: string } diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts index 1f9f9c5fa26a..d1b1d966820b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts @@ -1,8 +1,12 @@ +import CryptoJS from 'crypto-js' + import { + BadRequestException, Inject, Injectable, InternalServerErrorException, } from '@nestjs/common' +import { InjectModel } from '@nestjs/sequelize' import { FormatMessage, IntlService } from '@island.is/cms-translations' import type { Logger } from '@island.is/logging' @@ -10,10 +14,9 @@ import { LOGGER_PROVIDER } from '@island.is/logging' import { CaseFileCategory, - CaseState, EventType, - type IndictmentConfirmation, - isCompletedCase, + hasIndictmentCaseBeenSubmittedToCourt, + isTrafficViolationCase, type User as TUser, } from '@island.is/judicial-system/types' @@ -24,10 +27,9 @@ import { getCustodyNoticePdfAsBuffer, getRequestPdfAsBuffer, getRulingPdfAsBuffer, + IndictmentConfirmation, } from '../../formatters' -import { createConfirmedIndictment } from '../../formatters/confirmedIndictmentPdf' import { AwsS3Service } from '../aws-s3' -import { EventLogService } from '../event-log' import { UserService } from '../user' import { Case } from './models/case.model' @@ -38,8 +40,8 @@ export class PDFService { constructor( private readonly awsS3Service: AwsS3Service, private readonly intlService: IntlService, - private readonly eventLogService: EventLogService, private readonly userService: UserService, + @InjectModel(Case) private readonly caseModel: typeof Case, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -87,36 +89,50 @@ export class PDFService { ) ?.map((caseFile) => async () => { const buffer = await this.awsS3Service - .getObject(caseFile.key ?? '') + .getObject(theCase.type, theCase.state, caseFile.key) .catch((reason) => { // Tolerate failure, but log error this.logger.error( `Unable to get file ${caseFile.id} of case ${theCase.id} from AWS S3`, { reason }, ) + + return undefined }) return { chapter: caseFile.chapter as number, date: caseFile.displayDate ?? caseFile.created, name: caseFile.userGeneratedFilename ?? caseFile.name, - buffer: buffer ?? undefined, + buffer: buffer, } }) - return createCaseFilesRecord( + const generatedPdf = await createCaseFilesRecord( theCase, policeCaseNumber, caseFiles ?? [], this.formatMessage, ) + + if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state)) { + // No need to wait for the upload to finish + this.tryUploadPdfToS3( + theCase, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + generatedPdf, + ) + } + + return generatedPdf } async getCourtRecordPdf(theCase: Case, user: TUser): Promise { if (theCase.courtRecordSignatureDate) { try { - return await this.awsS3Service.getObject( - `generated/${theCase.id}/courtRecord.pdf`, + return await this.awsS3Service.getGeneratedObject( + theCase.type, + `${theCase.id}/courtRecord.pdf`, ) } catch (error) { this.logger.info( @@ -140,8 +156,9 @@ export class PDFService { async getRulingPdf(theCase: Case): Promise { if (theCase.rulingSignatureDate) { try { - return await this.awsS3Service.getObject( - `generated/${theCase.id}/ruling.pdf`, + return await this.awsS3Service.getGeneratedObject( + theCase.type, + `${theCase.id}/ruling.pdf`, ) } catch (error) { this.logger.info( @@ -162,62 +179,101 @@ export class PDFService { return getCustodyNoticePdfAsBuffer(theCase, this.formatMessage) } + private async tryGetPdfFromS3( + theCase: Case, + key: string, + ): Promise { + return await this.awsS3Service + .getObject(theCase.type, theCase.state, key) + .catch(() => undefined) // Ignore errors and return undefined + } + + private tryUploadPdfToS3(theCase: Case, key: string, pdf: Buffer) { + this.awsS3Service + .putObject(theCase.type, theCase.state, key, pdf.toString('binary')) + .catch((reason) => { + this.logger.error(`Failed to upload pdf ${key} to AWS S3`, { reason }) + }) + } + async getIndictmentPdf(theCase: Case): Promise { - await this.refreshFormatMessage() + if (!isTrafficViolationCase(theCase)) { + throw new BadRequestException( + `Case ${theCase.id} is not a traffic violation case`, + ) + } - let confirmation: IndictmentConfirmation = undefined - const confirmationEvent = await this.eventLogService.findEventTypeByCaseId( - EventType.INDICTMENT_CONFIRMED, - theCase.id, - ) + let confirmation: IndictmentConfirmation | undefined = undefined - if (confirmationEvent && confirmationEvent.nationalId) { - const actor = await this.userService.findByNationalId( - confirmationEvent.nationalId, + if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state)) { + if (theCase.indictmentHash) { + const existingPdf = await this.tryGetPdfFromS3( + theCase, + `${theCase.id}/indictment.pdf`, + ) + + if (existingPdf) { + return existingPdf + } + } + + const confirmationEvent = theCase.eventLogs?.find( + (event) => event.eventType === EventType.INDICTMENT_CONFIRMED, ) - confirmation = { - actor: actor.name, - institution: actor.institution?.name ?? '', - date: confirmationEvent.created, + if (confirmationEvent && confirmationEvent.nationalId) { + const actor = await this.userService.findByNationalId( + confirmationEvent.nationalId, + ) + + confirmation = { + actor: actor.name, + institution: actor.institution?.name ?? '', + date: confirmationEvent.created, + } } } - return createIndictment(theCase, this.formatMessage, confirmation) - } + await this.refreshFormatMessage() - async getConfirmedIndictmentPdf( - confirmation: IndictmentConfirmation, - indictmentPDF: Buffer, - ): Promise { - return createConfirmedIndictment(confirmation, indictmentPDF) + const generatedPdf = await createIndictment( + theCase, + this.formatMessage, + confirmation, + ) + + if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state) && confirmation) { + const indictmentHash = CryptoJS.MD5( + generatedPdf.toString('binary'), + ).toString(CryptoJS.enc.Hex) + + // No need to wait for this to finish + this.caseModel + .update({ indictmentHash }, { where: { id: theCase.id } }) + .then(() => + this.tryUploadPdfToS3( + theCase, + `${theCase.id}/indictment.pdf`, + generatedPdf, + ), + ) + } + + return generatedPdf } async getCaseFilesRecordPdf( theCase: Case, policeCaseNumber: string, ): Promise { - if ( - ![CaseState.NEW, CaseState.DRAFT, CaseState.SUBMITTED].includes( - theCase.state, + if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state)) { + const existingPdf = await this.tryGetPdfFromS3( + theCase, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, ) - ) { - if (isCompletedCase(theCase.state)) { - try { - return await this.awsS3Service.getObject( - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - } catch { - // Ignore the error and try the original key - } - } - try { - return await this.awsS3Service.getObject( - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - } catch { - // Ignore the error and generate the pdf + if (existingPdf) { + return existingPdf } } @@ -226,6 +282,6 @@ export class PDFService { policeCaseNumber, ) - return this.throttle + return await this.throttle } } diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts index 9231ba71d22e..8114e399c09c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts @@ -748,8 +748,6 @@ describe('Transition Case', () => { const allowedFromStates = [ CaseState.DRAFT, CaseState.WAITING_FOR_CONFIRMATION, - CaseState.SUBMITTED, - CaseState.RECEIVED, ] describe.each(allowedFromStates)( diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts index 8115f7da33a4..fbae47a66af0 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts @@ -101,8 +101,6 @@ const indictmentCaseStateMachine: Map< fromStates: [ IndictmentCaseState.DRAFT, IndictmentCaseState.WAITING_FOR_CONFIRMATION, - IndictmentCaseState.SUBMITTED, - IndictmentCaseState.RECEIVED, ], to: { state: IndictmentCaseState.DELETED }, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getAll.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getAll.spec.ts index bc96214b669d..9abf249fb601 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getAll.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getAll.spec.ts @@ -49,7 +49,7 @@ describe('CaseController - Get all', () => { const mockGetCasesQueryFilter = getCasesQueryFilter as jest.Mock mockGetCasesQueryFilter.mockReturnValueOnce(filter) const mockFindAll = mockCaseModel.findAll as jest.Mock - mockFindAll.mockReturnValueOnce(cases) + mockFindAll.mockResolvedValueOnce(cases) then = await givenWhenThen() }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdf.spec.ts index 79e03ac601df..b982b0dfba58 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdf.spec.ts @@ -3,7 +3,11 @@ import { uuid } from 'uuidv4' import { BadRequestException } from '@nestjs/common' -import { CaseFileCategory, CaseState } from '@island.is/judicial-system/types' +import { + CaseFileCategory, + CaseState, + CaseType, +} from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -38,11 +42,12 @@ describe('CaseController - Get case files record pdf', () => { ] as CaseFile[] const theCase = { id: caseId, - state: CaseState.ACCEPTED, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, policeCaseNumbers: [uuid(), policeCaseNumber, uuid()], caseFiles, } as Case - const pdf = uuid() + const pdf = Buffer.from(uuid()) const res = { end: jest.fn() } as unknown as Response let mockawsS3Service: AwsS3Service @@ -54,6 +59,8 @@ describe('CaseController - Get case files record pdf', () => { mockawsS3Service = awsS3Service const mockGetObject = mockawsS3Service.getObject as jest.Mock mockGetObject.mockRejectedValue(new Error('Some error')) + const mockPutObject = mockawsS3Service.putObject as jest.Mock + mockPutObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async (policeCaseNumber: string) => { const then = {} as Then @@ -82,13 +89,10 @@ describe('CaseController - Get case files record pdf', () => { }) it('should generate pdf after failing to get it from AWS S3', () => { - expect(mockawsS3Service.getObject).toHaveBeenNthCalledWith( - 1, - `indictments/completed/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - expect(mockawsS3Service.getObject).toHaveBeenNthCalledWith( - 2, - `indictments/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, + expect(mockawsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, ) expect(createCaseFilesRecord).toHaveBeenCalledWith( theCase, @@ -96,28 +100,20 @@ describe('CaseController - Get case files record pdf', () => { expect.any(Array), expect.any(Function), ) + expect(mockawsS3Service.putObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, + pdf.toString('binary'), + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) - describe('pdf returned from AWS S3 indictment completed folder', () => { - beforeEach(async () => { - const mockGetObject = mockawsS3Service.getObject as jest.Mock - mockGetObject.mockReturnValueOnce(pdf) - - await givenWhenThen(policeCaseNumber) - }) - - it('should return pdf', () => { - expect(res.end).toHaveBeenCalledWith(pdf) - }) - }) - - describe('pdf returned from AWS S3 indictment folder', () => { + describe('pdf returned from AWS S3', () => { beforeEach(async () => { const mockGetObject = mockawsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some error')) - mockGetObject.mockReturnValueOnce(pdf) + mockGetObject.mockResolvedValueOnce(pdf) await givenWhenThen(policeCaseNumber) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordPdf.spec.ts index b2e92770f532..143d68c2ab8b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordPdf.spec.ts @@ -3,7 +3,7 @@ import { uuid } from 'uuidv4' import { Logger } from '@island.is/logging' -import { User } from '@island.is/judicial-system/types' +import { CaseState, CaseType, User } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -33,9 +33,16 @@ describe('CaseController - Get court record pdf', () => { beforeEach(async () => { const { awsS3Service, logger, caseController } = await createTestingCaseModule() + mockAwsS3Service = awsS3Service mockLogger = logger + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) + const getMock = getCourtRecordPdfAsBuffer as jest.Mock + getMock.mockRejectedValue(new Error('Some error')) + givenWhenThen = async ( caseId: string, user: User, @@ -57,23 +64,29 @@ describe('CaseController - Get court record pdf', () => { describe('AWS S3 pdf returned', () => { const user = { id: uuid() } as User const caseId = uuid() + const caseType = CaseType.PAROLE_REVOCATION + const caseSate = CaseState.ACCEPTED const theCase = { id: caseId, + type: caseType, + state: caseSate, courtRecordSignatureDate: nowFactory(), } as Case const res = { end: jest.fn() } as unknown as Response const pdf = uuid() beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, user, theCase, res) }) it('should lookup pdf', () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/courtRecord.pdf`, + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/courtRecord.pdf`, ) expect(res.end).toHaveBeenCalledWith(pdf) }) @@ -86,13 +99,11 @@ describe('CaseController - Get court record pdf', () => { id: caseId, courtRecordSignatureDate: nowFactory(), } as Case - const error = new Error('Some ignored error') + const error = new Error('Some error') const res = { end: jest.fn() } as unknown as Response const pdf = uuid() beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(error) const getMock = getCourtRecordPdfAsBuffer as jest.Mock getMock.mockResolvedValueOnce(pdf) @@ -124,8 +135,6 @@ describe('CaseController - Get court record pdf', () => { const res = {} as Response beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) const getMock = getCourtRecordPdfAsBuffer as jest.Mock getMock.mockRejectedValueOnce(new Error('Some error')) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordSignatureConfirmation.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordSignatureConfirmation.spec.ts index 1edc06628158..59bc8a99c6a4 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordSignatureConfirmation.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCourtRecordSignatureConfirmation.spec.ts @@ -38,6 +38,14 @@ describe('CaseController - Get court record signature confirmation', () => { mockAwsS3Service = awsS3Service mockCaseModel = caseModel + const mockPutGeneratedObject = + mockAwsS3Service.putGeneratedObject as jest.Mock + mockPutGeneratedObject.mockRejectedValue(new Error('Some error')) + const mockUpdate = mockCaseModel.update as jest.Mock + mockUpdate.mockRejectedValue(new Error('Some error')) + const mockFindOne = mockCaseModel.findOne as jest.Mock + mockFindOne.mockRejectedValue(new Error('Some error')) + const mockTransaction = sequelize.transaction as jest.Mock transaction = {} as Transaction mockTransaction.mockImplementationOnce( @@ -92,8 +100,9 @@ describe('CaseController - Get court record signature confirmation', () => { let then: Then beforeEach(async () => { - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockResolvedValueOnce(Promise.resolve()) + const mockPutGeneratedObject = + mockAwsS3Service.putGeneratedObject as jest.Mock + mockPutGeneratedObject.mockResolvedValueOnce(Promise.resolve()) const mockUpdate = mockCaseModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [theCase]]) const mockFindOne = mockCaseModel.findOne as jest.Mock @@ -120,9 +129,6 @@ describe('CaseController - Get court record signature confirmation', () => { let then: Then beforeEach(async () => { - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, user, theCase, documentToken) }) @@ -138,10 +144,9 @@ describe('CaseController - Get court record signature confirmation', () => { let then: Then beforeEach(async () => { - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockResolvedValueOnce(Promise.resolve()) - const mockUpdate = mockCaseModel.update as jest.Mock - mockUpdate.mockRejectedValueOnce(new Error('Some error')) + const mockPutGeneratedObject = + mockAwsS3Service.putGeneratedObject as jest.Mock + mockPutGeneratedObject.mockResolvedValueOnce(Promise.resolve()) then = await givenWhenThen(caseId, user, theCase, documentToken) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdf.spec.ts index 5cc6729c4027..015d1f3c01ff 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdf.spec.ts @@ -1,9 +1,16 @@ import { Response } from 'express' import { uuid } from 'uuidv4' +import { + CaseState, + CaseType, + IndictmentSubtype, +} from '@island.is/judicial-system/types' + import { createTestingCaseModule } from '../createTestingCaseModule' import { createIndictment } from '../../../../formatters' +import { AwsS3Service } from '../../../aws-s3' import { Case } from '../../models/case.model' jest.mock('../../../../formatters/indictmentPdf') @@ -16,16 +23,29 @@ type GivenWhenThen = () => Promise describe('CaseController - Get indictment pdf', () => { const caseId = uuid() + const policeCaseNumber = uuid() const theCase = { id: caseId, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, + policeCaseNumbers: [policeCaseNumber], + indictmentSubtypes: { + [policeCaseNumber]: [IndictmentSubtype.TRAFFIC_VIOLATION], + }, + indictmentHash: uuid(), } as Case - const pdf = uuid() + const pdf = Buffer.from(uuid()) const res = { end: jest.fn() } as unknown as Response + let mockAwsS3Service: AwsS3Service let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { caseController } = await createTestingCaseModule() + const { awsS3Service, caseController } = await createTestingCaseModule() + + mockAwsS3Service = awsS3Service + const mockGetObject = mockAwsS3Service.getObject as jest.Mock + mockGetObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async () => { const then = {} as Then @@ -48,7 +68,12 @@ describe('CaseController - Get indictment pdf', () => { await givenWhenThen() }) - it('should generate pdf', () => { + it('should generate pdf after failing to get it from AWS S3', () => { + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/indictment.pdf`, + ) expect(createIndictment).toHaveBeenCalledWith( theCase, expect.any(Function), @@ -57,4 +82,17 @@ describe('CaseController - Get indictment pdf', () => { expect(res.end).toHaveBeenCalledWith(pdf) }) }) + + describe('pdf returned from AWS S3', () => { + beforeEach(async () => { + const mockGetObject = mockAwsS3Service.getObject as jest.Mock + mockGetObject.mockResolvedValueOnce(pdf) + + await givenWhenThen() + }) + + it('should return pdf', () => { + expect(res.end).toHaveBeenCalledWith(pdf) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingPdf.spec.ts index 4be69ad5d793..0f081c9a98ed 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingPdf.spec.ts @@ -3,6 +3,8 @@ import { uuid } from 'uuidv4' import { Logger } from '@island.is/logging' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingCaseModule } from '../createTestingCaseModule' import { nowFactory } from '../../../../factories' @@ -30,9 +32,16 @@ describe('CaseController - Get ruling pdf', () => { beforeEach(async () => { const { awsS3Service, logger, caseController } = await createTestingCaseModule() + mockAwsS3Service = awsS3Service mockLogger = logger + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) + const getMock = getRulingPdfAsBuffer as jest.Mock + getMock.mockRejectedValue(new Error('Some error')) + givenWhenThen = async (caseId: string, theCase: Case, res: Response) => { const then = {} as Then @@ -46,36 +55,32 @@ describe('CaseController - Get ruling pdf', () => { } }) - describe('AWS S3 lookup', () => { - const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = {} as Response - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, res) - }) - - it('should lookup pdf', () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/ruling.pdf`, - ) - }) - }) - describe('AWS S3 pdf returned', () => { const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case + const caseType = CaseType.EXPULSION_FROM_HOME + const caseState = CaseState.REJECTED + const theCase = { + id: caseId, + type: caseType, + state: caseState, + rulingSignatureDate: nowFactory(), + } as Case const res = { end: jest.fn() } as unknown as Response const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, theCase, res) }) it('should return pdf', () => { + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/ruling.pdf`, + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -84,12 +89,9 @@ describe('CaseController - Get ruling pdf', () => { const caseId = uuid() const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case const res = {} as Response - const error = new Error('Some ignored error') + const error = new Error('Some error') beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(error) - await givenWhenThen(caseId, theCase, res) }) @@ -101,43 +103,25 @@ describe('CaseController - Get ruling pdf', () => { }) }) - describe('pdf generated', () => { - const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = {} as Response - - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - - await givenWhenThen(caseId, theCase, res) - }) - - it('should generate pdf', () => { - expect(getRulingPdfAsBuffer).toHaveBeenCalledWith( - theCase, - expect.any(Function), - ) - }) - }) - - describe('pdf generated', () => { + describe('generated pdf returned', () => { const caseId = uuid() const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = {} as Response + const res = { end: jest.fn() } as unknown as Response + const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) + const getMock = getRulingPdfAsBuffer as jest.Mock + getMock.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, theCase, res) }) - it('should generate pdf', () => { + it('should return pdf', () => { expect(getRulingPdfAsBuffer).toHaveBeenCalledWith( theCase, expect.any(Function), ) + expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -158,26 +142,6 @@ describe('CaseController - Get ruling pdf', () => { }) }) - describe('generated pdf returned', () => { - const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = { end: jest.fn() } as unknown as Response - const pdf = {} - - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - const getMock = getRulingPdfAsBuffer as jest.Mock - getMock.mockResolvedValueOnce(pdf) - - await givenWhenThen(caseId, theCase, res) - }) - - it('should return pdf', () => { - expect(res.end).toHaveBeenCalledWith(pdf) - }) - }) - describe('pdf generation fails', () => { const caseId = uuid() const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case @@ -185,11 +149,6 @@ describe('CaseController - Get ruling pdf', () => { const res = {} as Response beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - const getMock = getRulingPdfAsBuffer as jest.Mock - getMock.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, theCase, res) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingSignatureConfirmation.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingSignatureConfirmation.spec.ts index 6ebe21d948ae..c05595c977b2 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingSignatureConfirmation.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getRulingSignatureConfirmation.spec.ts @@ -65,8 +65,9 @@ describe('CaseController - Get ruling signature confirmation', () => { const mockToday = nowFactory as jest.Mock mockToday.mockReturnValueOnce(date) - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockResolvedValue(uuid()) + const mockPutGeneratedObject = + mockAwsS3Service.putGeneratedObject as jest.Mock + mockPutGeneratedObject.mockResolvedValue(uuid()) const mockUpdate = mockCaseModel.update as jest.Mock mockUpdate.mockResolvedValue([1]) const mockPostMessageToQueue = @@ -125,15 +126,12 @@ describe('CaseController - Get ruling signature confirmation', () => { then = await givenWhenThen(caseId, user, theCase, documentToken) }) - it('should set the ruling signature date', () => { + it('should return success', () => { expect(mockCaseModel.update).toHaveBeenCalledWith( { rulingSignatureDate: date }, { where: { id: caseId }, transaction }, ) - }) - - it('should return success', () => { - expect(mockAwsS3Service.putObject).toHaveBeenCalled() + expect(mockAwsS3Service.putGeneratedObject).toHaveBeenCalled() expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ { type: MessageType.DELIVERY_TO_COURT_SIGNED_RULING, user, caseId }, { @@ -169,10 +167,7 @@ describe('CaseController - Get ruling signature confirmation', () => { { rulingSignatureDate: date }, { where: { id: caseId }, transaction }, ) - }) - - it('should return success', () => { - expect(mockAwsS3Service.putObject).toHaveBeenCalled() + expect(mockAwsS3Service.putGeneratedObject).toHaveBeenCalled() expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ { type: MessageType.DELIVERY_TO_COURT_SIGNED_RULING, user, caseId }, { @@ -269,8 +264,9 @@ describe('CaseController - Get ruling signature confirmation', () => { let then: Then beforeEach(async () => { - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockRejectedValueOnce(new Error('Some error')) + const mockPutGeneratedObject = + mockAwsS3Service.putGeneratedObject as jest.Mock + mockPutGeneratedObject.mockRejectedValueOnce(new Error('Some error')) then = await givenWhenThen(caseId, user, theCase, documentToken) }) @@ -279,7 +275,6 @@ describe('CaseController - Get ruling signature confirmation', () => { expect(then.result.documentSigned).toBe(false) expect(then.result.message).toBeTruthy() expect(then.result.code).toBeUndefined() - expect(mockCaseModel.update).not.toHaveBeenCalled() expect(mockMessageService.sendMessagesToQueue).not.toHaveBeenCalled() }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts index ac0f883e4f10..5a3ff83919e4 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/transition.spec.ts @@ -301,8 +301,6 @@ describe('CaseController - Transition', () => { ${CaseTransition.COMPLETE} | ${CaseState.RECEIVED} | ${CaseState.COMPLETED} ${CaseTransition.DELETE} | ${CaseState.DRAFT} | ${CaseState.DELETED} ${CaseTransition.DELETE} | ${CaseState.WAITING_FOR_CONFIRMATION} | ${CaseState.DELETED} - ${CaseTransition.DELETE} | ${CaseState.SUBMITTED} | ${CaseState.DELETED} - ${CaseTransition.DELETE} | ${CaseState.RECEIVED} | ${CaseState.DELETED} `.describe( '$transition $oldState case transitioning to $newState case', ({ transition, oldState, newState }) => { @@ -359,12 +357,22 @@ describe('CaseController - Transition', () => { transition === CaseTransition.DELETE ? null : undefined, courtCaseNumber: transition === CaseTransition.RETURN_INDICTMENT - ? '' + ? null + : undefined, + indictmentHash: + transition === CaseTransition.RETURN_INDICTMENT + ? null : undefined, rulingDate: transition === CaseTransition.COMPLETE ? date : undefined, judgeId: transition === CaseTransition.REDISTRIBUTE ? null : undefined, + indictmentDeniedExplanation: + transition === CaseTransition.SUBMIT ? null : undefined, + indictmentReturnedExplanation: + transition === CaseTransition.ASK_FOR_CONFIRMATION + ? null + : undefined, }, { where: { id: caseId }, transaction }, ) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts index 117142cff466..3d4b3d6fba78 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts @@ -190,6 +190,7 @@ describe('CaseController - Update', () => { it('should delete a case file', () => { expect(mockFileService.deleteCaseFile).toHaveBeenCalledWith( + theCase, caseFile, transaction, ) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archiveCaseFilesRecord.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archiveCaseFilesRecord.spec.ts index 11ed25dccfda..9ae4785ad11c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archiveCaseFilesRecord.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archiveCaseFilesRecord.spec.ts @@ -2,6 +2,8 @@ import { uuid } from 'uuidv4' import { BadRequestException } from '@nestjs/common' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingCaseModule } from '../createTestingCaseModule' import { AwsS3Service } from '../../../aws-s3' @@ -20,6 +22,8 @@ type GivenWhenThen = ( describe('InternalCaseController - Archive case files record', () => { const caseId = uuid() + const caseType = CaseType.INDICTMENT + const caseState = CaseState.COMPLETED const policeCaseNumber = uuid() let mockawsS3Service: AwsS3Service @@ -30,10 +34,8 @@ describe('InternalCaseController - Archive case files record', () => { await createTestingCaseModule() mockawsS3Service = awsS3Service - const mockCopyObject = mockawsS3Service.copyObject as jest.Mock - mockCopyObject.mockRejectedValue(new Error('Some error')) - const mockDeleteObject = mockawsS3Service.deleteObject as jest.Mock - mockDeleteObject.mockRejectedValue(new Error('Some error')) + const mockArchiveObject = mockawsS3Service.archiveObject as jest.Mock + mockArchiveObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async ( policeCaseNumber: string, @@ -44,6 +46,8 @@ describe('InternalCaseController - Archive case files record', () => { await internalCaseController .archiveCaseFilesRecord(caseId, policeCaseNumber, { id: caseId, + type: caseType, + state: caseState, policeCaseNumbers, } as Case) .then((result) => (then.result = result)) @@ -57,8 +61,8 @@ describe('InternalCaseController - Archive case files record', () => { let then: Then beforeEach(async () => { - const mockCopyObject = mockawsS3Service.copyObject as jest.Mock - mockCopyObject.mockResolvedValueOnce(uuid()) + const mockArchiveObject = mockawsS3Service.archiveObject as jest.Mock + mockArchiveObject.mockResolvedValueOnce(uuid()) then = await givenWhenThen(policeCaseNumber, [ uuid(), @@ -67,20 +71,12 @@ describe('InternalCaseController - Archive case files record', () => { ]) }) - it('should copy the case files record to the AWS S3 indictment completed folder', () => { - expect(mockawsS3Service.copyObject).toHaveBeenCalledWith( - `indictments/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, - `indictments/completed/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - }) - - it('should delete the case files record from the AWS S3 indictment folder', () => { - expect(mockawsS3Service.deleteObject).toHaveBeenCalledWith( - `indictments/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, + it('should archive the case files record', () => { + expect(mockawsS3Service.archiveObject).toHaveBeenCalledWith( + caseType, + caseState, + `${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, ) - }) - - it('should return a success response', () => { expect(then.result).toEqual({ delivered: true }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverAppealToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverAppealToPolice.spec.ts index e00a9c2cf05a..fe04e77ebbf0 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverAppealToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverAppealToPolice.spec.ts @@ -14,7 +14,7 @@ import { createTestingCaseModule } from '../createTestingCaseModule' import { randomDate } from '../../../../test' import { AwsS3Service } from '../../../aws-s3' -import { CourtDocumentType, PoliceService } from '../../../police' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -40,8 +40,8 @@ describe('InternalCaseController - Deliver appeal to police', () => { mockAwsS3Service = awsS3Service mockPoliceService = policeService - const mockGetObject = awsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValue(new Error('Some error')) + const mockGetGeneratedObject = awsS3Service.getObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock mockUpdatePoliceCase.mockRejectedValue(new Error('Some error')) @@ -87,8 +87,8 @@ describe('InternalCaseController - Deliver appeal to police', () => { let then: Then beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(appealRulingPdf) + const mockGetGeneratedObject = mockAwsS3Service.getObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(appealRulingPdf) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock mockUpdatePoliceCase.mockResolvedValueOnce(true) @@ -96,7 +96,11 @@ describe('InternalCaseController - Deliver appeal to police', () => { then = await givenWhenThen(caseId, theCase) }) it('should update the police case', async () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(appealRulingKey) + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + caseType, + caseState, + appealRulingKey, + ) expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( user, caseId, @@ -109,7 +113,7 @@ describe('InternalCaseController - Deliver appeal to police', () => { caseConclusion, [ { - type: CourtDocumentType.RVUL, + type: PoliceDocumentType.RVUL, courtDocument: Base64.btoa(appealRulingPdf), }, ], diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourt.spec.ts index ee2a309ee87c..f8275ed779fc 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourt.spec.ts @@ -2,7 +2,7 @@ import { uuid } from 'uuidv4' import { BadRequestException } from '@nestjs/common' -import { CaseState, User } from '@island.is/judicial-system/types' +import { CaseState, CaseType, User } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -33,7 +33,8 @@ describe('InternalCaseController - Deliver case files record to court', () => { const courtCaseNumber = uuid() const theCase = { id: caseId, - state: CaseState.ACCEPTED, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, policeCaseNumbers: [policeCaseNumber], courtId, courtCaseNumber, @@ -45,9 +46,6 @@ describe('InternalCaseController - Deliver case files record to court', () => { let givenWhenThen: GivenWhenThen beforeEach(async () => { - const mockGet = createCaseFilesRecord as jest.Mock - mockGet.mockRejectedValue(new Error('Some error')) - const { awsS3Service, courtService, internalCaseController } = await createTestingCaseModule() @@ -94,37 +92,24 @@ describe('InternalCaseController - Deliver case files record to court', () => { then = await givenWhenThen(caseId, policeCaseNumber, theCase) }) - it('should try to get the pdf from AWS S3 indictment completed folder', () => { - expect(mockAwsS3Service.getObject).toHaveBeenNthCalledWith( - 1, - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + it('should deliver the case files record', () => { + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, ) - }) - - it('should try to get the pdf from AWS S3 indictment folder', () => { - expect(mockAwsS3Service.getObject).toHaveBeenNthCalledWith( - 2, - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - }) - - it('should generate the case files record', async () => { expect(createCaseFilesRecord).toHaveBeenCalledWith( theCase, policeCaseNumber, [], expect.any(Function), ) - }) - - it('should store the case files record in AWS S3', async () => { expect(mockAwsS3Service.putObject).toHaveBeenCalledWith( - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + theCase.type, + theCase.state, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, pdf.toString(), ) - }) - - it('should create a case files record at court', async () => { expect(mockCourtService.createDocument).toHaveBeenCalledWith( user, caseId, @@ -136,41 +121,14 @@ describe('InternalCaseController - Deliver case files record to court', () => { 'application/pdf', pdf, ) - }) - - it('should return a success response', async () => { expect(then.result).toEqual({ delivered: true }) }) }) - describe('pdf returned from AWS S3 indictment completed folder', () => { - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockReturnValueOnce(pdf) - - await givenWhenThen(caseId, policeCaseNumber, theCase) - }) - - it('should use the AWS S3 pdf', () => { - expect(mockCourtService.createDocument).toHaveBeenCalledWith( - user, - caseId, - courtId, - courtCaseNumber, - CourtDocumentFolder.CASE_DOCUMENTS, - `Skjalaskrá ${policeCaseNumber}`, - `Skjalaskrá ${policeCaseNumber}.pdf`, - 'application/pdf', - pdf, - ) - }) - }) - - describe('pdf returned from AWS S3 indictment folder', () => { + describe('pdf returned from AWS S3', () => { beforeEach(async () => { const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some error')) - mockGetObject.mockReturnValueOnce(pdf) + mockGetObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, policeCaseNumber, theCase) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourtGuards.spec.ts index e0b918dcb4c3..328109144d6c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourtGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToCourtGuards.spec.ts @@ -1,5 +1,3 @@ -import { CanActivate } from '@nestjs/common' - import { indictmentCases } from '@island.is/judicial-system/types' import { CaseExistsGuard } from '../../guards/caseExists.guard' @@ -17,34 +15,12 @@ describe('InternalCaseController - Deliver case files record to court guards', ( ) }) - it('should have two guards', () => { + it('should have the right guard configuration', () => { expect(guards).toHaveLength(2) - }) - - describe('CaseExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[0]() - }) - - it('should have CaseExistsGuard as guard 1', () => { - expect(guard).toBeInstanceOf(CaseExistsGuard) - }) - }) - - describe('CaseTypeGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = guards[1] - }) - - it('should have CaseTypeGuard as guard 2', () => { - expect(guard).toBeInstanceOf(CaseTypeGuard) - expect(guard).toEqual({ - allowedCaseTypes: indictmentCases, - }) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: indictmentCases, }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToPolice.spec.ts index 57d1eb6f335b..860e0e59dfd1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseFilesRecordToPolice.spec.ts @@ -11,7 +11,7 @@ import { nowFactory } from '../../../../factories' import { createCaseFilesRecord } from '../../../../formatters' import { randomDate } from '../../../../test' import { AwsS3Service } from '../../../aws-s3' -import { CourtDocumentType, PoliceService } from '../../../police' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -34,7 +34,7 @@ describe('InternalCaseController - Deliver case files record to police', () => { const user = { id: uuid() } as User const caseId = uuid() const caseType = CaseType.INDICTMENT - const caseState = CaseState.ACCEPTED + const caseState = CaseState.COMPLETED const policeCaseNumber = uuid() const defendantNationalId = '0123456789' const courtId = uuid() @@ -107,13 +107,10 @@ describe('InternalCaseController - Deliver case files record to police', () => { }) it('should update the police case', () => { - expect(mockAwsS3Service.getObject).toHaveBeenNthCalledWith( - 1, - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - expect(mockAwsS3Service.getObject).toHaveBeenNthCalledWith( - 2, - `indictments/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + caseType, + caseState, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, ) expect(createCaseFilesRecord).toHaveBeenCalledWith( theCase, @@ -122,7 +119,9 @@ describe('InternalCaseController - Deliver case files record to police', () => { expect.any(Function), ) expect(mockAwsS3Service.putObject).toHaveBeenCalledWith( - `indictments/completed/${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, + theCase.type, + theCase.state, + `${theCase.id}/${policeCaseNumber}/caseFilesRecord.pdf`, pdf.toString(), ) expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( @@ -137,7 +136,7 @@ describe('InternalCaseController - Deliver case files record to police', () => { '', [ { - type: CourtDocumentType.RVMG, + type: PoliceDocumentType.RVMG, courtDocument: Base64.btoa(pdf.toString('binary')), }, ], @@ -146,40 +145,10 @@ describe('InternalCaseController - Deliver case files record to police', () => { }) }) - describe('pdf returned from AWS S3 indictment completed folder', () => { - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockReturnValueOnce(pdf) - - await givenWhenThen(caseId, policeCaseNumber, theCase) - }) - - it('should use the AWS S3 pdf', () => { - expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( - user, - caseId, - caseType, - caseState, - policeCaseNumber, - courtCaseNumber, - defendantNationalId, - date, - '', - [ - { - type: CourtDocumentType.RVMG, - courtDocument: Base64.btoa(pdf.toString('binary')), - }, - ], - ) - }) - }) - - describe('pdf returned from AWS S3 indictment folder', () => { + describe('pdf returned from AWS S3', () => { beforeEach(async () => { const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some error')) - mockGetObject.mockReturnValueOnce(pdf) + mockGetObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, policeCaseNumber, theCase) }) @@ -197,7 +166,7 @@ describe('InternalCaseController - Deliver case files record to police', () => { '', [ { - type: CourtDocumentType.RVMG, + type: PoliceDocumentType.RVMG, courtDocument: Base64.btoa(pdf.toString('binary')), }, ], diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseToPolice.spec.ts index d26aa17ec415..65ad3532e525 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverCaseToPolice.spec.ts @@ -16,7 +16,7 @@ import { getRequestPdfAsString, } from '../../../../formatters' import { randomDate } from '../../../../test' -import { CourtDocumentType, PoliceService } from '../../../police' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -130,15 +130,15 @@ describe('InternalCaseController - Deliver case to police', () => { caseConclusion, [ { - type: CourtDocumentType.RVKR, + type: PoliceDocumentType.RVKR, courtDocument: Base64.btoa(requestPdf), }, { - type: CourtDocumentType.RVTB, + type: PoliceDocumentType.RVTB, courtDocument: Base64.btoa(courtRecordPdf), }, { - type: CourtDocumentType.RVVI, + type: PoliceDocumentType.RVVI, courtDocument: Base64.btoa(custodyNoticePdf), }, ], diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentCaseToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentCaseToPolice.spec.ts index 8bc07564f842..9897732717a4 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentCaseToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentCaseToPolice.spec.ts @@ -14,7 +14,7 @@ import { createTestingCaseModule } from '../createTestingCaseModule' import { nowFactory } from '../../../../factories' import { randomDate } from '../../../../test' import { AwsS3Service } from '../../../aws-s3' -import { CourtDocumentType, PoliceService } from '../../../police' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -101,8 +101,16 @@ describe('InternalCaseController - Deliver indictment case to police', () => { }) it('should update the police case', async () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(courtRecordKey) - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(rulingKey) + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + caseType, + caseState, + courtRecordKey, + ) + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + caseType, + caseState, + rulingKey, + ) expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( user, caseId, @@ -115,11 +123,11 @@ describe('InternalCaseController - Deliver indictment case to police', () => { '', [ { - type: CourtDocumentType.RVTB, + type: PoliceDocumentType.RVTB, courtDocument: Base64.btoa(courtRecordPdf), }, { - type: CourtDocumentType.RVDO, + type: PoliceDocumentType.RVDO, courtDocument: Base64.btoa(rulingPdf), }, ], diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourt.spec.ts new file mode 100644 index 000000000000..0bcb6dcd14ba --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourt.spec.ts @@ -0,0 +1,166 @@ +import { uuid } from 'uuidv4' + +import { + CaseState, + CaseType, + IndictmentSubtype, + User, +} from '@island.is/judicial-system/types' + +import { createTestingCaseModule } from '../createTestingCaseModule' + +import { createIndictment } from '../../../../formatters' +import { AwsS3Service } from '../../../aws-s3' +import { CourtDocumentFolder, CourtService } from '../../../court' +import { Case } from '../../models/case.model' +import { DeliverResponse } from '../../models/deliver.response' + +jest.mock('../../../../formatters/indictmentPdf') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = (caseId: string, theCase: Case) => Promise + +describe('InternalCaseController - Deliver indictment to court', () => { + const user = { id: uuid() } as User + const caseId = uuid() + const policeCaseNumber = uuid() + const courtId = uuid() + const courtCaseNumber = uuid() + const theCase = { + id: caseId, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, + policeCaseNumbers: [policeCaseNumber], + indictmentSubtypes: { + [policeCaseNumber]: [IndictmentSubtype.TRAFFIC_VIOLATION], + }, + courtId, + courtCaseNumber, + indictmentHash: uuid(), + } as Case + const pdf = Buffer.from('test indictment') + + let mockAwsS3Service: AwsS3Service + let mockCourtService: CourtService + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { awsS3Service, courtService, internalCaseController } = + await createTestingCaseModule() + + mockAwsS3Service = awsS3Service + const mockGetObject = mockAwsS3Service.getObject as jest.Mock + mockGetObject.mockRejectedValue(new Error('Some error')) + + const mockCreateIndictment = createIndictment as jest.Mock + mockCreateIndictment.mockRejectedValue(new Error('Some error')) + + mockCourtService = courtService + const mockCreateDocument = mockCourtService.createDocument as jest.Mock + mockCreateDocument.mockRejectedValue(new Error('Some error')) + + givenWhenThen = async (caseId: string, theCase: Case) => { + const then = {} as Then + + await internalCaseController + .deliverIndictmentToCourt(caseId, theCase, { user }) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('deliver generated indictment pdf to court', () => { + let then: Then + + beforeEach(async () => { + const mockCreateIndictment = createIndictment as jest.Mock + mockCreateIndictment.mockResolvedValueOnce(pdf) + const mockCreateDocument = mockCourtService.createDocument as jest.Mock + mockCreateDocument.mockResolvedValueOnce(uuid()) + + then = await givenWhenThen(caseId, theCase) + }) + + it('should deliver the indictment', () => { + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${theCase.id}/indictment.pdf`, + ) + expect(createIndictment).toHaveBeenCalledWith( + theCase, + expect.any(Function), + undefined, + ) + expect(mockCourtService.createDocument).toHaveBeenCalledWith( + user, + caseId, + courtId, + courtCaseNumber, + CourtDocumentFolder.INDICTMENT_DOCUMENTS, + `Ákæra`, + `Ákæra.pdf`, + 'application/pdf', + pdf, + ) + expect(then.result).toEqual({ delivered: true }) + }) + }) + + describe('deliver indictment pdf from AWS S3 to court', () => { + beforeEach(async () => { + const mockGetGeneratedIndictmentCaseObject = + mockAwsS3Service.getObject as jest.Mock + mockGetGeneratedIndictmentCaseObject.mockResolvedValueOnce(pdf) + + await givenWhenThen(caseId, theCase) + }) + + it('should use the AWS S3 pdf', () => { + expect(mockCourtService.createDocument).toHaveBeenCalledWith( + user, + caseId, + courtId, + courtCaseNumber, + CourtDocumentFolder.INDICTMENT_DOCUMENTS, + `Ákæra`, + `Ákæra.pdf`, + 'application/pdf', + pdf, + ) + }) + }) + + describe('delivery to court fails', () => { + let then: Then + + beforeEach(async () => { + const mockCreateIndictment = createIndictment as jest.Mock + mockCreateIndictment.mockResolvedValueOnce(pdf) + + then = await givenWhenThen(caseId, theCase) + }) + + it('should return a failure response', async () => { + expect(then.result.delivered).toEqual(false) + }) + }) + + describe('pdf generation fails', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen(caseId, theCase) + }) + + it('should return a failure response', async () => { + expect(then.result.delivered).toEqual(false) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourtGuards.spec.ts new file mode 100644 index 000000000000..897190bc36a4 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToCourtGuards.spec.ts @@ -0,0 +1,26 @@ +import { indictmentCases } from '@island.is/judicial-system/types' + +import { CaseExistsGuard } from '../../guards/caseExists.guard' +import { CaseTypeGuard } from '../../guards/caseType.guard' +import { InternalCaseController } from '../../internalCase.controller' + +describe('InternalCaseController - Deliver indictment to court guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalCaseController.prototype.deliverIndictmentToCourt, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(2) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPolice.spec.ts index 6eab6494afed..a9bf45ec1fb2 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPolice.spec.ts @@ -6,6 +6,7 @@ import { CaseOrigin, CaseState, CaseType, + IndictmentSubtype, User, } from '@island.is/judicial-system/types' @@ -15,7 +16,8 @@ import { nowFactory } from '../../../../factories' import { createIndictment } from '../../../../formatters' import { randomDate } from '../../../../test' import { AwsS3Service } from '../../../aws-s3' -import { CourtDocumentType, PoliceService } from '../../../police' +import { FileService } from '../../../file' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -35,20 +37,24 @@ describe('InternalCaseController - Deliver indictment to police', () => { const user = { id: userId } as User let mockAwsS3Service: AwsS3Service + let mockFileService: FileService let mockPoliceService: PoliceService let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { awsS3Service, policeService, internalCaseController } = + const { awsS3Service, fileService, policeService, internalCaseController } = await createTestingCaseModule() mockAwsS3Service = awsS3Service + mockFileService = fileService mockPoliceService = policeService const mockToday = nowFactory as jest.Mock mockToday.mockReturnValueOnce(date) - const mockGetObject = awsS3Service.getObject as jest.Mock + const mockGetObject = mockAwsS3Service.getObject as jest.Mock mockGetObject.mockRejectedValue(new Error('Some error')) + const mockGetCaseFileFromS3 = mockFileService.getCaseFileFromS3 as jest.Mock + mockGetCaseFileFromS3.mockRejectedValue(new Error('Some error')) const mockCreateIndictment = createIndictment as jest.Mock mockCreateIndictment.mockRejectedValue(new Error('Some error')) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock @@ -71,12 +77,17 @@ describe('InternalCaseController - Deliver indictment to police', () => { describe('deliver indictment case files to police', () => { const caseId = uuid() const caseType = CaseType.INDICTMENT - const caseState = CaseState.ACCEPTED + const caseState = CaseState.WAITING_FOR_CONFIRMATION const policeCaseNumber = uuid() const courtCaseNumber = uuid() const defendantNationalId = '0123456789' const indictmentKey = uuid() const indictmentPdf = 'test indictment' + const caseFile = { + id: uuid(), + key: indictmentKey, + category: CaseFileCategory.INDICTMENT, + } const theCase = { id: caseId, origin: CaseOrigin.LOKE, @@ -85,16 +96,15 @@ describe('InternalCaseController - Deliver indictment to police', () => { policeCaseNumbers: [policeCaseNumber], courtCaseNumber, defendants: [{ nationalId: defendantNationalId }], - caseFiles: [ - { key: indictmentKey, category: CaseFileCategory.INDICTMENT }, - ], + caseFiles: [caseFile], } as Case let then: Then beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(indictmentPdf) + const mockGetCaseFileFromS3 = + mockFileService.getCaseFileFromS3 as jest.Mock + mockGetCaseFileFromS3.mockResolvedValueOnce(indictmentPdf) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock mockUpdatePoliceCase.mockResolvedValueOnce(true) @@ -103,7 +113,10 @@ describe('InternalCaseController - Deliver indictment to police', () => { }) it('should update the police case', async () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(indictmentKey) + expect(mockFileService.getCaseFileFromS3).toHaveBeenCalledWith( + theCase, + caseFile, + ) expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( user, caseId, @@ -116,7 +129,7 @@ describe('InternalCaseController - Deliver indictment to police', () => { '', [ { - type: CourtDocumentType.RVAS, + type: PoliceDocumentType.RVAS, courtDocument: Base64.btoa(indictmentPdf), }, ], @@ -128,7 +141,7 @@ describe('InternalCaseController - Deliver indictment to police', () => { describe('deliver generated indictment pdf to police', () => { const caseId = uuid() const caseType = CaseType.INDICTMENT - const caseState = CaseState.ACCEPTED + const caseState = CaseState.COMPLETED const policeCaseNumber = uuid() const courtCaseNumber = uuid() const defendantNationalId = '0123456789' @@ -141,6 +154,9 @@ describe('InternalCaseController - Deliver indictment to police', () => { policeCaseNumbers: [policeCaseNumber], courtCaseNumber, defendants: [{ nationalId: defendantNationalId }], + indictmentSubtypes: { + [policeCaseNumber]: [IndictmentSubtype.TRAFFIC_VIOLATION], + }, } as Case let then: Then @@ -173,7 +189,7 @@ describe('InternalCaseController - Deliver indictment to police', () => { '', [ { - type: CourtDocumentType.RVAS, + type: PoliceDocumentType.RVAS, courtDocument: Base64.btoa(indictmentPdf), }, ], @@ -181,4 +197,55 @@ describe('InternalCaseController - Deliver indictment to police', () => { expect(then.result.delivered).toEqual(true) }) }) + + describe('deliver indictment pdf from AWS S3 to police', () => { + const caseId = uuid() + const caseType = CaseType.INDICTMENT + const caseState = CaseState.COMPLETED + const policeCaseNumber = uuid() + const courtCaseNumber = uuid() + const defendantNationalId = '0123456789' + const indictmentPdf = 'test indictment' + const theCase = { + id: caseId, + origin: CaseOrigin.LOKE, + type: caseType, + state: caseState, + policeCaseNumbers: [policeCaseNumber], + courtCaseNumber, + defendants: [{ nationalId: defendantNationalId }], + indictmentSubtypes: { + [policeCaseNumber]: [IndictmentSubtype.TRAFFIC_VIOLATION], + }, + indictmentHash: uuid(), + } as Case + + beforeEach(async () => { + const mockGetGeneratedIndictmentCaseObject = + mockAwsS3Service.getObject as jest.Mock + mockGetGeneratedIndictmentCaseObject.mockResolvedValueOnce(indictmentPdf) + + await givenWhenThen(caseId, theCase) + }) + + it('should update the police case', async () => { + expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( + user, + caseId, + caseType, + caseState, + policeCaseNumber, + courtCaseNumber, + defendantNationalId, + date, + '', + [ + { + type: PoliceDocumentType.RVAS, + courtDocument: Base64.btoa(indictmentPdf), + }, + ], + ) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPoliceGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPoliceGuards.spec.ts index 3d389c82009d..454c56553c2b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPoliceGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentToPoliceGuards.spec.ts @@ -1,5 +1,3 @@ -import { CanActivate } from '@nestjs/common' - import { indictmentCases } from '@island.is/judicial-system/types' import { CaseExistsGuard } from '../../guards/caseExists.guard' @@ -17,34 +15,12 @@ describe('InternalCaseController - Deliver indictment to police guards', () => { ) }) - it('should have two guards', () => { + it('should have the right guard configuration', () => { expect(guards).toHaveLength(2) - }) - - describe('CaseExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[0]() - }) - - it('should have CaseExistsGuard as guard 1', () => { - expect(guard).toBeInstanceOf(CaseExistsGuard) - }) - }) - - describe('CaseTypeGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = guards[1] - }) - - it('should have CaseTypeGuard as guard 2', () => { - expect(guard).toBeInstanceOf(CaseTypeGuard) - expect(guard).toEqual({ - allowedCaseTypes: indictmentCases, - }) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: indictmentCases, }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToCourt.spec.ts index 30df480ec2c0..b0d1b3e91c19 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToCourt.spec.ts @@ -1,7 +1,7 @@ import format from 'date-fns/format' import { uuid } from 'uuidv4' -import { User } from '@island.is/judicial-system/types' +import { CaseState, CaseType, User } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -38,8 +38,9 @@ describe('InternalCaseController - Deliver signed ruling to court', () => { mockCreateDocument.mockRejectedValue(new Error('Some error')) mockAwsS3Service = awsS3Service - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValue(new Error('Some error')) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async (caseId: string, theCase: Case) => { const then = {} as Then @@ -56,8 +57,17 @@ describe('InternalCaseController - Deliver signed ruling to court', () => { describe('signed ruling delivered', () => { const caseId = uuid() const courtId = uuid() + const caseType = CaseType.BODY_SEARCH + const caseState = CaseState.ACCEPTED + const courtCaseNumber = uuid() - const theCase = { id: caseId, courtId, courtCaseNumber } as Case + const theCase = { + id: caseId, + type: caseType, + state: caseState, + courtId, + courtCaseNumber, + } as Case const pdf = Buffer.from('test ruling') const now = randomDate() @@ -66,21 +76,20 @@ describe('InternalCaseController - Deliver signed ruling to court', () => { beforeEach(async () => { const mockNowFactory = nowFactory as jest.Mock mockNowFactory.mockReturnValue(now) - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) const mockCreateDocument = mockCourtService.createDocument as jest.Mock mockCreateDocument.mockResolvedValueOnce(uuid()) then = await givenWhenThen(caseId, theCase) }) - it('should get the signed ruling from S3', async () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/ruling.pdf`, + it('should deliver the signed ruling to court', async () => { + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/ruling.pdf`, ) - }) - - it('should create a ruling at court', async () => { expect(mockCourtService.createDocument).toHaveBeenCalledWith( user, caseId, @@ -92,9 +101,6 @@ describe('InternalCaseController - Deliver signed ruling to court', () => { 'application/pdf', pdf, ) - }) - - it('should return a success response', async () => { expect(then.result.delivered).toEqual(true) }) }) @@ -108,8 +114,9 @@ describe('InternalCaseController - Deliver signed ruling to court', () => { let then: Then beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) then = await givenWhenThen(caseId, theCase) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToPolice.spec.ts index bfc2c4634e90..6085395e9d23 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverSignedRulingToPolice.spec.ts @@ -12,7 +12,7 @@ import { createTestingCaseModule } from '../createTestingCaseModule' import { randomDate } from '../../../../test' import { AwsS3Service } from '../../../aws-s3' -import { CourtDocumentType, PoliceService } from '../../../police' +import { PoliceDocumentType, PoliceService } from '../../../police' import { Case } from '../../models/case.model' import { DeliverResponse } from '../../models/deliver.response' @@ -38,8 +38,8 @@ describe('InternalCaseController - Deliver signed ruling to police', () => { mockAwsS3Service = awsS3Service mockPoliceService = policeService - const mockGetObject = awsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValue(new Error('Some error')) + const mockGetGeneratedObject = awsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock mockUpdatePoliceCase.mockRejectedValue(new Error('Some error')) @@ -80,8 +80,9 @@ describe('InternalCaseController - Deliver signed ruling to police', () => { let then: Then beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(rulingPdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(rulingPdf) const mockUpdatePoliceCase = mockPoliceService.updatePoliceCase as jest.Mock mockUpdatePoliceCase.mockResolvedValueOnce(true) @@ -90,8 +91,9 @@ describe('InternalCaseController - Deliver signed ruling to police', () => { }) it('should update the police case', async () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/ruling.pdf`, + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/ruling.pdf`, ) expect(mockPoliceService.updatePoliceCase).toHaveBeenCalledWith( user, @@ -105,7 +107,7 @@ describe('InternalCaseController - Deliver signed ruling to police', () => { caseConclusion, [ { - type: CourtDocumentType.RVUR, + type: PoliceDocumentType.RVUR, courtDocument: Base64.btoa(rulingPdf), }, ], diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdf.spec.ts index 6690c916730c..5a8fbdb69ce0 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdf.spec.ts @@ -3,7 +3,11 @@ import { uuid } from 'uuidv4' import { BadRequestException } from '@nestjs/common' -import { CaseFileCategory, CaseState } from '@island.is/judicial-system/types' +import { + CaseFileCategory, + CaseState, + CaseType, +} from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -38,11 +42,12 @@ describe('LimitedAccessCaseController - Get case files record pdf', () => { ] as CaseFile[] const theCase = { id: caseId, - state: CaseState.ACCEPTED, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, policeCaseNumbers: [uuid(), policeCaseNumber, uuid()], caseFiles, } as Case - const pdf = uuid() + const pdf = Buffer.from(uuid()) const res = { end: jest.fn() } as unknown as Response let mockawsS3Service: AwsS3Service @@ -55,6 +60,8 @@ describe('LimitedAccessCaseController - Get case files record pdf', () => { mockawsS3Service = awsS3Service const mockGetObject = mockawsS3Service.getObject as jest.Mock mockGetObject.mockRejectedValue(new Error('Some error')) + const mockPutObject = mockawsS3Service.putObject as jest.Mock + mockPutObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async (policeCaseNumber: string) => { const then = {} as Then @@ -83,13 +90,10 @@ describe('LimitedAccessCaseController - Get case files record pdf', () => { }) it('should generate pdf after failing to get it from AWS S3', () => { - expect(mockawsS3Service.getObject).toHaveBeenNthCalledWith( - 1, - `indictments/completed/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, - ) - expect(mockawsS3Service.getObject).toHaveBeenNthCalledWith( - 2, - `indictments/${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, + expect(mockawsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, ) expect(createCaseFilesRecord).toHaveBeenCalledWith( theCase, @@ -97,28 +101,20 @@ describe('LimitedAccessCaseController - Get case files record pdf', () => { expect.any(Array), expect.any(Function), ) + expect(mockawsS3Service.putObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/${policeCaseNumber}/caseFilesRecord.pdf`, + pdf.toString('binary'), + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) - describe('pdf returned from AWS S3 indictment completed folder', () => { - beforeEach(async () => { - const mockGetObject = mockawsS3Service.getObject as jest.Mock - mockGetObject.mockReturnValueOnce(pdf) - - await givenWhenThen(policeCaseNumber) - }) - - it('should return pdf', () => { - expect(res.end).toHaveBeenCalledWith(pdf) - }) - }) - - describe('pdf returned from AWS S3 indictment folder', () => { + describe('pdf returned from AWS S3', () => { beforeEach(async () => { const mockGetObject = mockawsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some error')) - mockGetObject.mockReturnValueOnce(pdf) + mockGetObject.mockResolvedValueOnce(pdf) await givenWhenThen(policeCaseNumber) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCourtRecordPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCourtRecordPdf.spec.ts index d624584fd8ea..2d642cd9d34a 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCourtRecordPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCourtRecordPdf.spec.ts @@ -3,7 +3,7 @@ import { uuid } from 'uuidv4' import { Logger } from '@island.is/logging' -import { User } from '@island.is/judicial-system/types' +import { CaseState, CaseType, User } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -33,9 +33,16 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { beforeEach(async () => { const { awsS3Service, logger, limitedAccessCaseController } = await createTestingCaseModule() + mockAwsS3Service = awsS3Service mockLogger = logger + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) + const getMock = getCourtRecordPdfAsBuffer as jest.Mock + getMock.mockRejectedValue(new Error('Some error')) + givenWhenThen = async ( caseId: string, user: User, @@ -59,44 +66,33 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { } }) - describe('AWS S3 lookup', () => { - const user = {} as User - const caseId = uuid() - const theCase = { - id: caseId, - courtRecordSignatureDate: nowFactory(), - } as Case - const res = {} as Response - - beforeEach(async () => { - await givenWhenThen(caseId, user, theCase, res) - }) - - it('should lookup pdf', () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/courtRecord.pdf`, - ) - }) - }) - describe('AWS S3 pdf returned', () => { const user = {} as User const caseId = uuid() + const caseType = CaseType.EXPULSION_FROM_HOME + const caseState = CaseState.DISMISSED const theCase = { id: caseId, + type: caseType, + state: caseState, courtRecordSignatureDate: nowFactory(), } as Case const res = { end: jest.fn() } as unknown as Response const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, user, theCase, res) }) it('should return pdf', () => { + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/courtRecord.pdf`, + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -109,12 +105,9 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { courtRecordSignatureDate: nowFactory(), } as Case const res = {} as Response - const error = new Error('Some ignored error') + const error = new Error('Some error') beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(error) - await givenWhenThen(caseId, user, theCase, res) }) @@ -126,31 +119,6 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { }) }) - describe('pdf generated', () => { - const user = {} as User - const caseId = uuid() - const theCase = { - id: caseId, - courtRecordSignatureDate: nowFactory(), - } as Case - const res = {} as Response - - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - - await givenWhenThen(caseId, user, theCase, res) - }) - - it('should generate pdf', () => { - expect(getCourtRecordPdfAsBuffer).toHaveBeenCalledWith( - theCase, - expect.any(Function), - user, - ) - }) - }) - describe('generated pdf returned', () => { const user = {} as User const caseId = uuid() @@ -162,8 +130,6 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) const getMock = getCourtRecordPdfAsBuffer as jest.Mock getMock.mockResolvedValueOnce(pdf) @@ -171,6 +137,11 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { }) it('should return pdf', () => { + expect(getCourtRecordPdfAsBuffer).toHaveBeenCalledWith( + theCase, + expect.any(Function), + user, + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -186,11 +157,6 @@ describe('LimitedAccessCaseController - Get court record pdf', () => { const res = {} as Response beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - const getMock = getCourtRecordPdfAsBuffer as jest.Mock - getMock.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, user, theCase, res) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdf.spec.ts index 6e40f0b0e114..026b70c71758 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdf.spec.ts @@ -1,9 +1,16 @@ import { Response } from 'express' import { uuid } from 'uuidv4' +import { + CaseState, + CaseType, + IndictmentSubtype, +} from '@island.is/judicial-system/types' + import { createTestingCaseModule } from '../createTestingCaseModule' import { createIndictment } from '../../../../formatters' +import { AwsS3Service } from '../../../aws-s3' import { Case } from '../../models/case.model' jest.mock('../../../../formatters/indictmentPdf') @@ -16,16 +23,30 @@ type GivenWhenThen = () => Promise describe('LimitedCaseController - Get indictment pdf', () => { const caseId = uuid() + const policeCaseNumber = uuid() const theCase = { id: caseId, + type: CaseType.INDICTMENT, + state: CaseState.COMPLETED, + policeCaseNumbers: [policeCaseNumber], + indictmentSubtypes: { + [policeCaseNumber]: [IndictmentSubtype.TRAFFIC_VIOLATION], + }, + indictmentHash: uuid(), } as Case - const pdf = uuid() + const pdf = Buffer.from(uuid()) const res = { end: jest.fn() } as unknown as Response + let mockawsS3Service: AwsS3Service let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { limitedAccessCaseController } = await createTestingCaseModule() + const { awsS3Service, limitedAccessCaseController } = + await createTestingCaseModule() + + mockawsS3Service = awsS3Service + const mockGetObject = mockawsS3Service.getObject as jest.Mock + mockGetObject.mockRejectedValue(new Error('Some error')) givenWhenThen = async () => { const then = {} as Then @@ -49,6 +70,11 @@ describe('LimitedCaseController - Get indictment pdf', () => { }) it('should generate pdf', () => { + expect(mockawsS3Service.getObject).toHaveBeenCalledWith( + theCase.type, + theCase.state, + `${caseId}/indictment.pdf`, + ) expect(createIndictment).toHaveBeenCalledWith( theCase, expect.any(Function), @@ -57,4 +83,17 @@ describe('LimitedCaseController - Get indictment pdf', () => { expect(res.end).toHaveBeenCalledWith(pdf) }) }) + + describe('pdf returned from AWS S3', () => { + beforeEach(async () => { + const mockGetObject = mockawsS3Service.getObject as jest.Mock + mockGetObject.mockResolvedValueOnce(pdf) + + await givenWhenThen() + }) + + it('should return pdf', () => { + expect(res.end).toHaveBeenCalledWith(pdf) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getRulingPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getRulingPdf.spec.ts index 3b19de1e2299..d7e5b8e6c89c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getRulingPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getRulingPdf.spec.ts @@ -3,6 +3,8 @@ import { uuid } from 'uuidv4' import { Logger } from '@island.is/logging' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingCaseModule } from '../createTestingCaseModule' import { nowFactory } from '../../../../factories' @@ -30,9 +32,16 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { beforeEach(async () => { const { awsS3Service, logger, limitedAccessCaseController } = await createTestingCaseModule() + mockAwsS3Service = awsS3Service mockLogger = logger + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockRejectedValue(new Error('Some error')) + const getMock = getRulingPdfAsBuffer as jest.Mock + getMock.mockRejectedValue(new Error('Some error')) + givenWhenThen = async (caseId: string, theCase: Case, res: Response) => { const then = {} as Then @@ -46,36 +55,32 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { } }) - describe('AWS S3 lookup', () => { - const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = {} as Response - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, res) - }) - - it('should lookup pdf', () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( - `generated/${caseId}/ruling.pdf`, - ) - }) - }) - describe('AWS S3 pdf returned', () => { const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case + const caseType = CaseType.AUTOPSY + const caseState = CaseState.REJECTED + const theCase = { + id: caseId, + type: caseType, + state: caseState, + rulingSignatureDate: nowFactory(), + } as Case const res = { end: jest.fn() } as unknown as Response const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockResolvedValueOnce(pdf) + const mockGetGeneratedObject = + mockAwsS3Service.getGeneratedObject as jest.Mock + mockGetGeneratedObject.mockResolvedValueOnce(pdf) await givenWhenThen(caseId, theCase, res) }) it('should return pdf', () => { + expect(mockAwsS3Service.getGeneratedObject).toHaveBeenCalledWith( + caseType, + `${caseId}/ruling.pdf`, + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -84,12 +89,9 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { const caseId = uuid() const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case const res = {} as Response - const error = new Error('Some ignored error') + const error = new Error('Some error') beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(error) - await givenWhenThen(caseId, theCase, res) }) @@ -101,26 +103,6 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { }) }) - describe('pdf generated', () => { - const caseId = uuid() - const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case - const res = {} as Response - - beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - - await givenWhenThen(caseId, theCase, res) - }) - - it('should generate pdf', () => { - expect(getRulingPdfAsBuffer).toHaveBeenCalledWith( - theCase, - expect.any(Function), - ) - }) - }) - describe('generated pdf returned', () => { const caseId = uuid() const theCase = { id: caseId, rulingSignatureDate: nowFactory() } as Case @@ -128,8 +110,6 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { const pdf = {} beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) const getMock = getRulingPdfAsBuffer as jest.Mock getMock.mockResolvedValueOnce(pdf) @@ -137,6 +117,10 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { }) it('should return pdf', () => { + expect(getRulingPdfAsBuffer).toHaveBeenCalledWith( + theCase, + expect.any(Function), + ) expect(res.end).toHaveBeenCalledWith(pdf) }) }) @@ -148,11 +132,6 @@ describe('LimitedAccessCaseController - Get ruling pdf', () => { const res = {} as Response beforeEach(async () => { - const mockGetObject = mockAwsS3Service.getObject as jest.Mock - mockGetObject.mockRejectedValueOnce(new Error('Some ignored error')) - const getMock = getRulingPdfAsBuffer as jest.Mock - getMock.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, theCase, res) }) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts index 30fdd92b23af..22e881540a50 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts @@ -5,12 +5,14 @@ import { Inject, Injectable, InternalServerErrorException, + NotFoundException, } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' +import { formatNationalId } from '@island.is/judicial-system/formatters' import { CaseMessage, MessageService, @@ -195,6 +197,37 @@ export class DefendantService { return updatedDefendant } + async updateByNationalId( + caseId: string, + defendantNationalId: string, + update: UpdateDefendantDto, + ): Promise { + const formattedNationalId = formatNationalId(defendantNationalId) + + const [numberOfAffectedRows, defendants] = await this.defendantModel.update( + update, + { + where: { + caseId, + [Op.or]: [ + { national_id: formattedNationalId }, + { national_id: defendantNationalId }, + ], + }, + returning: true, + }, + ) + + const updatedDefendant = this.getUpdatedDefendant( + numberOfAffectedRows, + defendants, + defendants[0].id, + caseId, + ) + + return updatedDefendant + } + async delete( theCase: Case, defendantId: string, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts index 31e4109c16d8..eee5ea58da99 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts @@ -2,7 +2,7 @@ import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' -import { Gender } from '@island.is/judicial-system/types' +import { DefenderChoice, Gender } from '@island.is/judicial-system/types' export class CreateDefendantDto { @IsOptional() @@ -56,7 +56,7 @@ export class CreateDefendantDto { readonly defenderPhoneNumber?: string @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly defendantWaivesRightToCounsel?: boolean + @IsEnum(DefenderChoice) + @ApiPropertyOptional({ enum: DefenderChoice }) + readonly defenderChoice?: DefenderChoice } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts index 922535aab96e..e9887e658203 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts @@ -4,6 +4,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger' import { DefendantPlea, + DefenderChoice, Gender, ServiceRequirement, } from '@island.is/judicial-system/types' @@ -60,9 +61,9 @@ export class UpdateDefendantDto { readonly defenderPhoneNumber?: string @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly defendantWaivesRightToCounsel?: boolean + @IsEnum(DefenderChoice) + @ApiPropertyOptional({ enum: DefenderChoice }) + readonly defenderChoice?: DefenderChoice @IsOptional() @IsEnum(DefendantPlea) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts index e4736064af61..1fbefa6b9af2 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts @@ -3,10 +3,11 @@ import { Controller, Inject, Param, + Patch, Post, UseGuards, } from '@nestjs/common' -import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger' +import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' @@ -19,26 +20,26 @@ import { import { Case, CaseExistsGuard, CurrentCase } from '../case' import { DeliverDefendantToCourtDto } from './dto/deliverDefendantToCourt.dto' +import { UpdateDefendantDto } from './dto/updateDefendant.dto' import { CurrentDefendant } from './guards/defendant.decorator' import { DefendantExistsGuard } from './guards/defendantExists.guard' import { Defendant } from './models/defendant.model' import { DeliverResponse } from './models/deliver.response' import { DefendantService } from './defendant.service' -@Controller( - `api/internal/case/:caseId/${ - messageEndpoint[MessageType.DELIVERY_TO_COURT_DEFENDANT] - }/:defendantId`, -) +@Controller('api/internal/case/:caseId') @ApiTags('internal defendants') -@UseGuards(TokenGuard, CaseExistsGuard, DefendantExistsGuard) +@UseGuards(TokenGuard, CaseExistsGuard) export class InternalDefendantController { constructor( private readonly defendantService: DefendantService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} - @Post() + @UseGuards(DefendantExistsGuard) + @Post( + `${messageEndpoint[MessageType.DELIVERY_TO_COURT_DEFENDANT]}/:defendantId`, + ) @ApiCreatedResponse({ type: DeliverResponse, description: 'Delivers a case file to court', @@ -60,4 +61,26 @@ export class InternalDefendantController { deliverDefendantToCourtDto.user, ) } + + @Patch('defense/:defendantNationalId') + @ApiOkResponse({ + type: Defendant, + description: 'Assigns defense choice to defendant', + }) + async assignDefender( + @Param('caseId') caseId: string, + @Param('defendantNationalId') defendantNationalId: string, + @CurrentCase() theCase: Case, + @Body() updatedDefendantChoice: UpdateDefendantDto, + ): Promise { + this.logger.debug(`Assigning defense choice to defendant in case ${caseId}`) + + const updatedDefendant = await this.defendantService.updateByNationalId( + theCase.id, + defendantNationalId, + updatedDefendantChoice, + ) + + return updatedDefendant + } } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts index 053d19ff4bf2..1d6b1e0f471e 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts @@ -13,6 +13,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { DefendantPlea, + DefenderChoice, Gender, ServiceRequirement, } from '@island.is/judicial-system/types' @@ -94,9 +95,13 @@ export class Defendant extends Model { @ApiPropertyOptional({ type: String }) defenderPhoneNumber?: string - @Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: false }) - @ApiProperty({ type: Boolean }) - defendantWaivesRightToCounsel!: boolean + @Column({ + type: DataType.ENUM, + allowNull: true, + values: Object.values(DefenderChoice), + }) + @ApiPropertyOptional({ enum: DefenderChoice }) + defenderChoice?: DefenderChoice @Column({ type: DataType.ENUM, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts index 6e88a771a9f5..ae0ad03605fb 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts @@ -14,8 +14,8 @@ describe('InternalDefendantController - guards', () => { guards = Reflect.getMetadata('__guards__', InternalDefendantController) }) - it('should have three guards', () => { - expect(guards).toHaveLength(3) + it('should have two guards', () => { + expect(guards).toHaveLength(2) }) describe('TokenGuard', () => { @@ -42,14 +42,14 @@ describe('InternalDefendantController - guards', () => { }) }) - describe('DefendantExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[2]() - }) - - it('should have DefendantExistsGuard as guard 3', () => { + describe('Method level guards', () => { + it('should have DefendantExistsGuard on deliverDefendantToCourt method', () => { + const methodGuards = Reflect.getMetadata( + '__guards__', + InternalDefendantController.prototype.deliverDefendantToCourt, + ) + expect(methodGuards).toHaveLength(1) + const guard = new methodGuards[0]() expect(guard).toBeInstanceOf(DefendantExistsGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts index 044af37f6c89..c1ee4450f943 100644 --- a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts +++ b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts @@ -1,3 +1,4 @@ +import { Transaction } from 'sequelize/types' import { Sequelize } from 'sequelize-typescript' import { Inject, Injectable } from '@nestjs/common' @@ -11,11 +12,12 @@ import { EventType } from '@island.is/judicial-system/types' import { CreateEventLogDto } from './dto/createEventLog.dto' import { EventLog } from './models/eventLog.model' -const allowMultiple = [ - 'LOGIN', - 'LOGIN_UNAUTHORIZED', - 'LOGIN_BYPASS', - 'LOGIN_BYPASS_UNAUTHORIZED', +const allowMultiple: EventType[] = [ + EventType.LOGIN, + EventType.LOGIN_UNAUTHORIZED, + EventType.LOGIN_BYPASS, + EventType.LOGIN_BYPASS_UNAUTHORIZED, + EventType.INDICTMENT_CONFIRMED, ] @Injectable() @@ -27,7 +29,10 @@ export class EventLogService { private readonly logger: Logger, ) {} - async create(event: CreateEventLogDto): Promise { + async create( + event: CreateEventLogDto, + transaction?: Transaction, + ): Promise { const { eventType, caseId, userRole, nationalId } = event if (!allowMultiple.includes(event.eventType)) { @@ -45,28 +50,16 @@ export class EventLogService { } try { - await this.eventLogModel.create({ - eventType, - caseId, - nationalId, - userRole, - }) + await this.eventLogModel.create( + { eventType, caseId, nationalId, userRole }, + { transaction }, + ) } catch (error) { // Tolerate failure but log error this.logger.error('Failed to create event log', error) } } - async findEventTypeByCaseId(eventType: EventType, caseId: string) { - return this.eventLogModel.findOne({ - where: { - eventType, - caseId, - }, - order: [['created', 'DESC']], - }) - } - async loginMap( nationalIds: string[], ): Promise> { diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts index c44a97392ed2..f507d84863e2 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts @@ -177,12 +177,13 @@ export class FileController { }) deleteCaseFile( @Param('caseId') caseId: string, + @CurrentCase() theCase: Case, @Param('fileId') fileId: string, @CurrentCaseFile() caseFile: CaseFile, ): Promise { this.logger.debug(`Deleting file ${fileId} of case ${caseId}`) - return this.fileService.deleteCaseFile(caseFile) + return this.fileService.deleteCaseFile(theCase, caseFile) } @UseGuards( @@ -208,7 +209,7 @@ export class FileController { ): Promise { this.logger.debug(`Uploading file ${fileId} of case ${caseId} to court`) - return this.fileService.uploadCaseFileToCourt(caseFile, theCase, user) + return this.fileService.uploadCaseFileToCourt(theCase, caseFile, user) } @UseGuards( diff --git a/apps/judicial-system/backend/src/app/modules/file/file.module.ts b/apps/judicial-system/backend/src/app/modules/file/file.module.ts index b76ea0352f28..54956e763846 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.module.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.module.ts @@ -5,7 +5,7 @@ import { CmsTranslationsModule } from '@island.is/cms-translations' import { MessageModule } from '@island.is/judicial-system/message' -import { AwsS3Module, CaseModule, CourtModule } from '../index' +import { AwsS3Module, CaseModule, CourtModule, UserModule } from '../index' import { CaseFile } from './models/file.model' import { FileController } from './file.controller' import { FileService } from './file.service' @@ -16,6 +16,7 @@ import { LimitedAccessFileController } from './limitedAccessFile.controller' imports: [ CmsTranslationsModule, MessageModule, + forwardRef(() => UserModule), forwardRef(() => CaseModule), forwardRef(() => CourtModule), forwardRef(() => AwsS3Module), diff --git a/apps/judicial-system/backend/src/app/modules/file/file.service.ts b/apps/judicial-system/backend/src/app/modules/file/file.service.ts index 804511ac1d3b..42f0f22f76f3 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.service.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.service.ts @@ -1,3 +1,4 @@ +import CryptoJS from 'crypto-js' import { Op, Sequelize } from 'sequelize' import { Transaction } from 'sequelize/types' import { uuid } from 'uuidv4' @@ -20,15 +21,16 @@ import type { User } from '@island.is/judicial-system/types' import { CaseFileCategory, CaseFileState, - CaseState, - completedIndictmentCaseStates, + EventType, + hasIndictmentCaseBeenSubmittedToCourt, isIndictmentCase, } from '@island.is/judicial-system/types' -import { formatConfirmedIndictmentKey } from '../../formatters/formatters' +import { createConfirmedIndictment } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { Case } from '../case' import { CourtDocumentFolder, CourtService } from '../court' +import { UserService } from '../user' import { CreateFileDto } from './dto/createFile.dto' import { CreatePresignedPostDto } from './dto/createPresignedPost.dto' import { UpdateFileDto } from './dto/updateFile.dto' @@ -40,13 +42,10 @@ import { SignedUrl } from './models/signedUrl.model' import { UploadFileToCourtResponse } from './models/uploadFileToCourt.response' import { fileModuleConfig } from './file.config' -// Files are stored in AWS S3 under a key which has the following formats: -// uploads/// for restriction and investigation cases +// File keys have the following format: +// // // As uuid-s have length 36, the filename starts at position 82 in the key. -const NAME_BEGINS_INDEX = 82 -// indictments/// for indictment cases -// As uuid-s have length 36, the filename starts at position 82 in the key. -const INDICTMENT_NAME_BEGINS_INDEX = 86 +const NAME_BEGINS_INDEX = 74 @Injectable() export class FileService { @@ -55,6 +54,7 @@ export class FileService { constructor( @InjectConnection() private readonly sequelize: Sequelize, @InjectModel(CaseFile) private readonly fileModel: typeof CaseFile, + private readonly userService: UserService, private readonly courtService: CourtService, private readonly awsS3Service: AwsS3Service, private readonly messageService: MessageService, @@ -91,22 +91,27 @@ export class FileService { return numberOfAffectedRows > 0 } - private async tryDeleteFileFromS3(file: CaseFile): Promise { + private async tryDeleteFileFromS3( + theCase: Case, + file: CaseFile, + ): Promise { this.logger.debug(`Attempting to delete file ${file.key} from AWS S3`) if (!file.key) { return true } - return this.awsS3Service.deleteObject(file.key).catch((reason) => { - // Tolerate failure, but log what happened - this.logger.error( - `Could not delete file ${file.id} of case ${file.caseId} from AWS S3`, - { reason }, - ) + return this.awsS3Service + .deleteObject(theCase.type, theCase.state, file.key) + .catch((reason) => { + // Tolerate failure, but log what happened + this.logger.error( + `Could not delete file ${file.id} of case ${file.caseId} from AWS S3`, + { reason }, + ) - return false - }) + return false + }) } private getCourtDocumentFolder(file: CaseFile) { @@ -144,6 +149,69 @@ export class FileService { return courtDocumentFolder } + private async confirmIndictmentCaseFile( + theCase: Case, + file: CaseFile, + pdf: Buffer, + ): Promise { + const confirmationEvent = theCase.eventLogs?.find( + (event) => event.eventType === EventType.INDICTMENT_CONFIRMED, + ) + + if (!confirmationEvent || !confirmationEvent.nationalId) { + return undefined + } + + return this.userService + .findByNationalId(confirmationEvent.nationalId) + .then((user) => + createConfirmedIndictment( + { + actor: user.name, + institution: user.institution?.name ?? '', + date: confirmationEvent.created, + }, + pdf, + ), + ) + .then((confirmedPdf) => { + const binaryPdf = confirmedPdf.toString('binary') + const hash = CryptoJS.MD5(binaryPdf).toString(CryptoJS.enc.Hex) + + // No need to wait for the update to finish + this.fileModel.update({ hash }, { where: { id: file.id } }) + + return binaryPdf + }) + .catch((reason) => { + this.logger.error( + `Failed to create confirmed indictment for case ${theCase.id}`, + { reason }, + ) + + return undefined + }) + } + + async getCaseFileFromS3(theCase: Case, file: CaseFile): Promise { + if ( + isIndictmentCase(theCase.type) && + hasIndictmentCaseBeenSubmittedToCourt(theCase.state) && + file.category === CaseFileCategory.INDICTMENT + ) { + return this.awsS3Service.getConfirmedObject( + theCase.type, + theCase.state, + file.key, + !file.hash, + (content: Buffer) => + this.confirmIndictmentCaseFile(theCase, file, content), + ) + } + + return this.awsS3Service.getObject(theCase.type, theCase.state, file.key) + } + private async throttleUpload( file: CaseFile, theCase: Case, @@ -154,7 +222,7 @@ export class FileService { this.logger.info('Previous upload failed', { reason }) }) - const content = await this.awsS3Service.getObject(file.key ?? '') + const content = await this.getCaseFileFromS3(theCase, file) const courtDocumentFolder = this.getCourtDocumentFolder(file) @@ -191,12 +259,14 @@ export class FileService { ): Promise { const { fileName, type } = createPresignedPost - return this.awsS3Service.createPresignedPost( - `${isIndictmentCase(theCase.type) ? 'indictments' : 'uploads'}/${ - theCase.id - }/${uuid()}/${fileName}`, - type, - ) + const key = `${theCase.id}/${uuid()}/${fileName}` + + return this.awsS3Service + .createPresignedPost(theCase.type, theCase.state, key, type) + .then((presignedPost) => ({ + ...presignedPost, + key, + })) } async createCaseFile( @@ -206,11 +276,7 @@ export class FileService { ): Promise { const { key } = createFile - const regExp = new RegExp( - `^${isIndictmentCase(theCase.type) ? 'indictments' : 'uploads'}/${ - theCase.id - }/.{36}/(.*)$`, - ) + const regExp = new RegExp(`^${theCase.id}/.{36}/(.*)$`) if (!regExp.test(key)) { throw new BadRequestException( @@ -218,11 +284,7 @@ export class FileService { ) } - const fileName = createFile.key.slice( - isIndictmentCase(theCase.type) - ? INDICTMENT_NAME_BEGINS_INDEX - : NAME_BEGINS_INDEX, - ) + const fileName = createFile.key.slice(NAME_BEGINS_INDEX) const file = await this.fileModel.create({ ...createFile, @@ -256,29 +318,16 @@ export class FileService { return file } - async getCaseFileSignedUrl( - theCase: Case, - file: CaseFile, - ): Promise { + private async verifyCaseFile(file: CaseFile, theCase: Case) { if (!file.key) { throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) } - let key = file.key - - if ( - file.category === CaseFileCategory.INDICTMENT && - [ - CaseState.SUBMITTED, - CaseState.RECEIVED, - CaseState.MAIN_HEARING, - ...completedIndictmentCaseStates, - ].includes(theCase.state) - ) { - key = formatConfirmedIndictmentKey(key) - } - - const exists = await this.awsS3Service.objectExists(key) + const exists = await this.awsS3Service.objectExists( + theCase.type, + theCase.state, + file.key, + ) if (!exists) { // Fire and forget, no need to wait for the result @@ -286,11 +335,50 @@ export class FileService { throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) } + } - return this.awsS3Service.getSignedUrl(key).then((url) => ({ url })) + private async getCaseFileSignedUrlFromS3( + theCase: Case, + file: CaseFile, + timeToLive?: number, + ): Promise { + if ( + isIndictmentCase(theCase.type) && + hasIndictmentCaseBeenSubmittedToCourt(theCase.state) && + file.category === CaseFileCategory.INDICTMENT + ) { + return this.awsS3Service.getConfirmedSignedUrl( + theCase.type, + theCase.state, + file.key, + !file.hash, + (content: Buffer) => + this.confirmIndictmentCaseFile(theCase, file, content), + timeToLive, + ) + } + + return this.awsS3Service.getSignedUrl( + theCase.type, + theCase.state, + file.key, + timeToLive, + ) + } + + async getCaseFileSignedUrl( + theCase: Case, + file: CaseFile, + ): Promise { + await this.verifyCaseFile(file, theCase) + + return this.getCaseFileSignedUrlFromS3(theCase, file).then((url) => ({ + url, + })) } async deleteCaseFile( + theCase: Case, file: CaseFile, transaction?: Transaction, ): Promise { @@ -298,33 +386,22 @@ export class FileService { if (success) { // Fire and forget, no need to wait for the result - this.tryDeleteFileFromS3(file) + this.tryDeleteFileFromS3(theCase, file) } return { success } } async uploadCaseFileToCourt( - file: CaseFile, theCase: Case, + file: CaseFile, user: User, ): Promise { if (file.state === CaseFileState.STORED_IN_COURT) { return { success: true } } - if (!file.key) { - throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) - } - - const exists = await this.awsS3Service.objectExists(file.key) - - if (!exists) { - // Fire and forget, no need to wait for the result - this.fileModel.update({ key: null }, { where: { id: file.id } }) - - throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) - } + await this.verifyCaseFile(file, theCase) this.throttle = this.throttleUpload(file, theCase, user) @@ -404,47 +481,13 @@ export class FileService { } async archive(theCase: Case, file: CaseFile): Promise { - if ( - !file.key || - !file.key.startsWith('indictments/') || - file.key.startsWith('indictments/completed/') - ) { + if (!file.key) { return true } return this.awsS3Service - .copyObject( - file.key, - file.key.replace('indictments/', 'indictments/completed/'), - ) - .then((newKey) => - this.fileModel.update({ key: newKey }, { where: { id: file.id } }), - ) - .then(async () => { - if ( - file.category === CaseFileCategory.INDICTMENT && - [ - CaseState.SUBMITTED, - CaseState.RECEIVED, - CaseState.MAIN_HEARING, - ...completedIndictmentCaseStates, - ].includes(theCase.state) - ) { - return this.awsS3Service.copyObject( - formatConfirmedIndictmentKey(file.key), - formatConfirmedIndictmentKey(file.key).replace( - 'indictments/', - 'indictments/completed/', - ) ?? '', - ) - } - }) - .then(() => { - // Fire and forget, no need to wait for the result - this.tryDeleteFileFromS3(file) - - return true - }) + .archiveObject(theCase.type, theCase.state, file.key) + .then(() => true) .catch((reason) => { this.logger.error( `Failed to archive file ${file.id} of case ${file.caseId}`, @@ -455,33 +498,30 @@ export class FileService { }) } - async resetCaseFileStates(caseId: string, transaction: Transaction) { - await this.fileModel.update( + resetCaseFileStates(caseId: string, transaction: Transaction) { + return this.fileModel.update( { state: CaseFileState.STORED_IN_RVG }, { where: { caseId, state: CaseFileState.STORED_IN_COURT }, transaction }, ) } + resetIndictmentCaseFileHashes(caseId: string, transaction: Transaction) { + return this.fileModel.update( + { hash: null }, + { where: { caseId, category: CaseFileCategory.INDICTMENT }, transaction }, + ) + } + async deliverCaseFileToCourtOfAppeals( - file: CaseFile, theCase: Case, + file: CaseFile, user: User, ): Promise { - if (!file.key) { - throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) - } - - const exists = await this.awsS3Service.objectExists(file.key) - - if (!exists) { - // Fire and forget, no need to wait for the result - this.fileModel.update({ key: null }, { where: { id: file.id } }) - - throw new NotFoundException(`File ${file.id} does not exist in AWS S3`) - } + await this.verifyCaseFile(file, theCase) - const url = await this.awsS3Service.getSignedUrl( - file.key ?? '', + const url = await this.getCaseFileSignedUrlFromS3( + theCase, + file, this.config.robotS3TimeToLiveGet, ) diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts index 0fa6e6fda3ce..3526675d6902 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts @@ -11,9 +11,8 @@ import { isCompletedCase, isDefenceUser, isIndictmentCase, - isInvestigationCase, isPrisonSystemUser, - isRestrictionCase, + isRequestCase, User, } from '@island.is/judicial-system/types' @@ -47,30 +46,32 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { throw new InternalServerErrorException('Missing case file') } - if (isCompletedCase(theCase.state) && caseFile.category) { - if (isDefenceUser(user)) { - if ( - (isRestrictionCase(theCase.type) || - isInvestigationCase(theCase.type)) && - defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( - caseFile.category, - ) - ) { - return true - } + if (isDefenceUser(user) && caseFile.category) { + if ( + isRequestCase(theCase.type) && + isCompletedCase(theCase.state) && + defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( + caseFile.category, + ) + ) { + return true + } + + if ( + isIndictmentCase(theCase.type) && + defenderCaseFileCategoriesForIndictmentCases.includes(caseFile.category) + ) { + return true + } + } - if ( - isIndictmentCase(theCase.type) && - defenderCaseFileCategoriesForIndictmentCases.includes( - caseFile.category, - ) - ) { - return true - } - } else if (isPrisonSystemUser(user)) { - if (caseFile.category === CaseFileCategory.APPEAL_RULING) { - return true - } + if (isPrisonSystemUser(user)) { + if ( + isCompletedCase(theCase.state) && + caseFile.category && + caseFile.category === CaseFileCategory.APPEAL_RULING + ) { + return true } } diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts index dbe541817035..2deebedc9db4 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts @@ -150,7 +150,7 @@ describe('Limited Access View Case File Guard', () => { ) describe.each(indictmentCases)('for %s cases', (type) => { - describe.each(completedCaseStates)('in state %s', (state) => { + describe.each(Object.values(CaseState))('in state %s', (state) => { const allowedCaseFileCategories = [ CaseFileCategory.COURT_RECORD, CaseFileCategory.RULING, @@ -208,36 +208,6 @@ describe('Limited Access View Case File Guard', () => { }) }) }) - - describe.each( - Object.keys(CaseState).filter( - (state) => !completedCaseStates.includes(state as CaseState), - ), - )('in state %s', (state) => { - describe.each(Object.keys(CaseFileCategory))( - 'a defender can not view %s', - (category) => { - let then: Then - - beforeEach(() => { - mockRequest.mockImplementationOnce(() => ({ - user: { role: UserRole.DEFENDER }, - case: { type, state }, - caseFile: { category }, - })) - - then = givenWhenThen() - }) - - it('should throw ForbiddenException', () => { - expect(then.error).toBeInstanceOf(ForbiddenException) - expect(then.error.message).toBe( - `Forbidden for ${UserRole.DEFENDER}`, - ) - }) - }, - ) - }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/internalFile.controller.ts b/apps/judicial-system/backend/src/app/modules/file/internalFile.controller.ts index c677eb2fc68c..964929db474f 100644 --- a/apps/judicial-system/backend/src/app/modules/file/internalFile.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/internalFile.controller.ts @@ -57,8 +57,8 @@ export class InternalFileController { this.logger.debug(`Delivering file ${fileId} of case ${caseId} to court`) const { success } = await this.fileService.uploadCaseFileToCourt( - caseFile, theCase, + caseFile, deliverDto.user, ) @@ -110,8 +110,8 @@ export class InternalFileController { ) return this.fileService.deliverCaseFileToCourtOfAppeals( - caseFile, theCase, + caseFile, deliverDto.user, ) } diff --git a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts index c729ee60ef19..3d8b09f65443 100644 --- a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts @@ -47,12 +47,7 @@ import { PresignedPost } from './models/presignedPost.model' import { SignedUrl } from './models/signedUrl.model' import { FileService } from './file.service' -@UseGuards( - JwtAuthGuard, - RolesGuard, - LimitedAccessCaseExistsGuard, - CaseCompletedGuard, -) +@UseGuards(JwtAuthGuard, RolesGuard, LimitedAccessCaseExistsGuard) @Controller('api/case/:caseId/limitedAccess') @ApiTags('files') export class LimitedAccessFileController { @@ -64,6 +59,7 @@ export class LimitedAccessFileController { @UseGuards( new CaseTypeGuard([...restrictionCases, ...investigationCases]), CaseWriteGuard, + CaseCompletedGuard, ) @RolesRules(defenderRule) @Post('file/url') @@ -84,6 +80,7 @@ export class LimitedAccessFileController { @UseGuards( new CaseTypeGuard([...restrictionCases, ...investigationCases]), CaseWriteGuard, + CaseCompletedGuard, LimitedAccessWriteCaseFileGuard, ) @RolesRules(defenderRule) @@ -92,7 +89,7 @@ export class LimitedAccessFileController { type: CaseFile, description: 'Creates a new case file', }) - async createCaseFile( + createCaseFile( @Param('caseId') caseId: string, @CurrentHttpUser() user: User, @CurrentCase() theCase: Case, @@ -126,6 +123,7 @@ export class LimitedAccessFileController { @UseGuards( new CaseTypeGuard([...restrictionCases, ...investigationCases]), CaseWriteGuard, + CaseCompletedGuard, CaseFileExistsGuard, LimitedAccessWriteCaseFileGuard, ) @@ -137,11 +135,12 @@ export class LimitedAccessFileController { }) deleteCaseFile( @Param('caseId') caseId: string, + @CurrentCase() theCase: Case, @Param('fileId') fileId: string, @CurrentCaseFile() caseFile: CaseFile, ): Promise { this.logger.debug(`Deleting file ${fileId} of case ${caseId}`) - return this.fileService.deleteCaseFile(caseFile) + return this.fileService.deleteCaseFile(theCase, caseFile) } } diff --git a/apps/judicial-system/backend/src/app/modules/file/models/file.model.ts b/apps/judicial-system/backend/src/app/modules/file/models/file.model.ts index d3e52c73ecd4..d144598e1122 100644 --- a/apps/judicial-system/backend/src/app/modules/file/models/file.model.ts +++ b/apps/judicial-system/backend/src/app/modules/file/models/file.model.ts @@ -100,4 +100,8 @@ export class CaseFile extends Model { @Column({ type: DataType.STRING, allowNull: true }) @ApiPropertyOptional({ type: String }) policeFileId?: string + + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + hash?: string } diff --git a/apps/judicial-system/backend/src/app/modules/file/models/presignedPost.model.ts b/apps/judicial-system/backend/src/app/modules/file/models/presignedPost.model.ts index 20335561b415..5374ba70034d 100644 --- a/apps/judicial-system/backend/src/app/modules/file/models/presignedPost.model.ts +++ b/apps/judicial-system/backend/src/app/modules/file/models/presignedPost.model.ts @@ -6,4 +6,7 @@ export class PresignedPost { @ApiProperty({ type: Object }) fields!: { [key: string]: string } + + @ApiProperty({ type: String }) + key!: string } diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFile.spec.ts index 6a53c85efd3e..88525e99b8c6 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFile.spec.ts @@ -68,7 +68,7 @@ describe('FileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.PROSECUTOR_APPEAL_STATEMENT_CASE_FILE, } @@ -76,7 +76,7 @@ describe('FileController - Create case file', () => { const timeStamp = randomDate() const caseFile = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.PROSECUTOR_APPEAL_STATEMENT_CASE_FILE, id: fileId, @@ -96,7 +96,7 @@ describe('FileController - Create case file', () => { expect(mockFileModel.create).toHaveBeenCalledWith({ type: 'text/plain', state: CaseFileState.STORED_IN_RVG, - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.PROSECUTOR_APPEAL_STATEMENT_CASE_FILE, caseId, @@ -122,14 +122,14 @@ describe('FileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } const fileId = uuid() const timeStamp = randomDate() const caseFile = { type: 'text/plain', - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, id: fileId, created: timeStamp, @@ -148,7 +148,7 @@ describe('FileController - Create case file', () => { expect(mockFileModel.create).toHaveBeenCalledWith({ type: 'text/plain', state: CaseFileState.STORED_IN_RVG, - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, caseId, name: 'test.txt', @@ -164,7 +164,7 @@ describe('FileController - Create case file', () => { const uuId = `-${uuid()}` const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } let then: Then @@ -176,7 +176,7 @@ describe('FileController - Create case file', () => { it('should throw bad gateway exception', () => { expect(then.error).toBeInstanceOf(BadRequestException) expect(then.error.message).toBe( - `uploads/${caseId}/${uuId}/test.txt is not a valid key for case ${caseId}`, + `${caseId}/${uuId}/test.txt is not a valid key for case ${caseId}`, ) }) }) @@ -187,7 +187,7 @@ describe('FileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } let then: Then diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPost.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPost.spec.ts index 18af35b86c17..1d265178b5da 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPost.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPost.spec.ts @@ -1,6 +1,7 @@ import { uuid } from 'uuidv4' import { + CaseState, indictmentCases, investigationCases, restrictionCases, @@ -53,7 +54,7 @@ describe('FileController - Create presigned post', () => { 'presigned post created for %s case', (type) => { const caseId = uuid() - const theCase = { id: caseId, type } as Case + const theCase = { id: caseId, type, state: CaseState.SUBMITTED } as Case const createPresignedPost: CreatePresignedPostDto = { fileName: 'test.txt', type: 'text/plain', @@ -63,11 +64,11 @@ describe('FileController - Create presigned post', () => { beforeEach(async () => { const mockCreatePresignedPost = mockAwsS3Service.createPresignedPost as jest.Mock - mockCreatePresignedPost.mockImplementationOnce((key: string) => + mockCreatePresignedPost.mockImplementationOnce((_1, _2, key: string) => Promise.resolve({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { - key, + key: `uploads/${key}`, bucket: 'island-is-dev-upload-judicial-system', 'X-Amz-Algorithm': 'Some Algorithm', 'X-Amz-Credential': 'Some Credentials', @@ -82,16 +83,14 @@ describe('FileController - Create presigned post', () => { then = await givenWhenThen(caseId, createPresignedPost, theCase) }) - it('should request a presigned post from AWS S3', () => { + it('should return a presigned post', () => { expect(mockAwsS3Service.createPresignedPost).toHaveBeenCalledWith( - expect.stringMatching( - new RegExp(`^uploads/${caseId}/.{36}/test.txt$`), - ), + type, + CaseState.SUBMITTED, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), 'text/plain', ) - }) - it('should return a presigned post', () => { expect(then.result).toEqual({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { @@ -104,6 +103,7 @@ describe('FileController - Create presigned post', () => { Policy: 'Some Policy', 'X-Amz-Signature': 'Some Signature', }, + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), }) expect(then.result.fields.key).toMatch( @@ -117,7 +117,11 @@ describe('FileController - Create presigned post', () => { 'presigned post created for %s case', (type) => { const caseId = uuid() - const theCase = { id: caseId, type } as Case + const theCase = { + id: caseId, + type, + state: CaseState.MAIN_HEARING, + } as Case const createPresignedPost: CreatePresignedPostDto = { fileName: 'test.txt', type: 'text/plain', @@ -127,11 +131,11 @@ describe('FileController - Create presigned post', () => { beforeEach(async () => { const mockCreatePresignedPost = mockAwsS3Service.createPresignedPost as jest.Mock - mockCreatePresignedPost.mockImplementationOnce((key: string) => + mockCreatePresignedPost.mockImplementationOnce((_1, _2, key: string) => Promise.resolve({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { - key, + key: `indictments/${key}`, bucket: 'island-is-dev-upload-judicial-system', 'X-Amz-Algorithm': 'Some Algorithm', 'X-Amz-Credential': 'Some Credentials', @@ -146,16 +150,14 @@ describe('FileController - Create presigned post', () => { then = await givenWhenThen(caseId, createPresignedPost, theCase) }) - it('should request a presigned post from AWS S3', () => { + it('should return a presigned post', () => { expect(mockAwsS3Service.createPresignedPost).toHaveBeenCalledWith( - expect.stringMatching( - new RegExp(`^indictments/${caseId}/.{36}/test.txt$`), - ), + type, + CaseState.MAIN_HEARING, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), 'text/plain', ) - }) - it('should return a presigned post', () => { expect(then.result).toEqual({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { @@ -168,6 +170,7 @@ describe('FileController - Create presigned post', () => { Policy: 'Some Policy', 'X-Amz-Signature': 'Some Signature', }, + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), }) expect(then.result.fields.key).toMatch( diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/deleteCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/deleteCaseFile.spec.ts index 892de744b60f..9252884e2ea5 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/deleteCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/deleteCaseFile.spec.ts @@ -1,10 +1,15 @@ import { uuid } from 'uuidv4' -import { CaseFileState } from '@island.is/judicial-system/types' +import { + CaseFileState, + CaseState, + CaseType, +} from '@island.is/judicial-system/types' import { createTestingFileModule } from '../createTestingFileModule' import { AwsS3Service } from '../../../aws-s3' +import { Case } from '../../../case' import { DeleteFileResponse } from '../../models/deleteFile.response' import { CaseFile } from '../../models/file.model' @@ -15,6 +20,7 @@ interface Then { type GivenWhenThen = ( caseId: string, + theCase: Case, fileId: string, casefile: CaseFile, ) => Promise @@ -36,13 +42,14 @@ describe('FileController - Delete case file', () => { givenWhenThen = async ( caseId: string, + theCase: Case, fileId: string, caseFile: CaseFile, ): Promise => { const then = {} as Then await fileController - .deleteCaseFile(caseId, fileId, caseFile) + .deleteCaseFile(caseId, theCase, fileId, caseFile) .then((result) => (then.result = result)) .catch((error) => (then.error = error)) @@ -52,41 +59,38 @@ describe('FileController - Delete case file', () => { describe('database update', () => { const caseId = uuid() + const caseType = CaseType.INDICTMENT + const caseState = CaseState.DRAFT + const theCase = { id: caseId, type: caseType, state: caseState } as Case const fileId = uuid() - const caseFile = { id: fileId } as CaseFile - - beforeEach(async () => { - await givenWhenThen(caseId, fileId, caseFile) - }) - - it('should update the case file status in the database', () => { - expect(mockFileModel.update).toHaveBeenCalledWith( - { state: CaseFileState.DELETED, key: null }, - { where: { id: fileId } }, - ) - }) - }) - - describe('AWS S3 removal', () => { - const caseId = uuid() - const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile + let then: Then beforeEach(async () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1]) - await givenWhenThen(caseId, fileId, caseFile) + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) - it('should attempt to remove from AWS S3', () => { - expect(mockAwsS3Service.deleteObject).toHaveBeenCalledWith(key) + it('should delete the case file', () => { + expect(mockFileModel.update).toHaveBeenCalledWith( + { state: CaseFileState.DELETED, key: null }, + { where: { id: fileId } }, + ) + expect(mockAwsS3Service.deleteObject).toHaveBeenCalledWith( + caseType, + caseState, + key, + ) + expect(then.result).toEqual({ success: true }) }) }) describe('AWS S3 removal skipped', () => { const caseId = uuid() + const theCase = { id: caseId } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile @@ -94,7 +98,7 @@ describe('FileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1]) - await givenWhenThen(caseId, fileId, caseFile) + await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should not attempt to remove from AWS S3', () => { @@ -102,26 +106,9 @@ describe('FileController - Delete case file', () => { }) }) - describe('case file deleted', () => { - const caseId = uuid() - const fileId = uuid() - const caseFile = { id: fileId } as CaseFile - let then: Then - - beforeEach(async () => { - const mockUpdate = mockFileModel.update as jest.Mock - mockUpdate.mockResolvedValueOnce([1]) - - then = await givenWhenThen(caseId, fileId, caseFile) - }) - - it('should return success', () => { - expect(then.result).toEqual({ success: true }) - }) - }) - describe('case file not deleted', () => { const caseId = uuid() + const theCase = { id: caseId } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile let then: Then @@ -130,7 +117,7 @@ describe('FileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([0]) - then = await givenWhenThen(caseId, fileId, caseFile) + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should return failure', () => { @@ -140,6 +127,7 @@ describe('FileController - Delete case file', () => { describe('database update fails', () => { const caseId = uuid() + const theCase = { id: caseId } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile let then: Then @@ -148,7 +136,7 @@ describe('FileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, fileId, caseFile) + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should throw error', () => { diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts index 5e158ac58ba2..6423672112cf 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts @@ -2,6 +2,8 @@ import { uuid } from 'uuidv4' import { NotFoundException } from '@nestjs/common' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingFileModule } from '../createTestingFileModule' import { AwsS3Service } from '../../../aws-s3' @@ -50,53 +52,17 @@ describe('FileController - Get case file signed url', () => { } }) - describe('AWS S3 existance check', () => { - const caseId = uuid() - const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` - const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case - let mockObjectExists: jest.Mock - - beforeEach(async () => { - mockObjectExists = mockAwsS3Service.objectExists as jest.Mock - - await givenWhenThen(caseId, theCase, fileId, caseFile) - }) - - it('should check if the file exists in AWS S3', () => { - expect(mockObjectExists).toHaveBeenCalledWith(key) - }) - }) - - describe('AWS S3 get signed url', () => { - const caseId = uuid() - const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` - const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case - let mockGetSignedUrl: jest.Mock - - beforeEach(async () => { - mockGetSignedUrl = mockAwsS3Service.getSignedUrl as jest.Mock - const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock - mockObjectExists.mockResolvedValueOnce(true) - - await givenWhenThen(caseId, theCase, fileId, caseFile) - }) - - it('should get signed url from AWS S3', () => { - expect(mockGetSignedUrl).toHaveBeenCalledWith(key) - }) - }) - describe('signed url created', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - const theCase = { id: uuid() } as Case - const url = uuid() + const theCase = { + id: uuid(), + type: CaseType.ADMISSION_TO_FACILITY, + state: CaseState.RECEIVED, + } as Case + const url = `uploads/${key}` let then: Then beforeEach(async () => { @@ -108,34 +74,32 @@ describe('FileController - Get case file signed url', () => { then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) - it('should return the signed url', () => { + it('should create a signed url', () => { + expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + ) + expect(mockAwsS3Service.getSignedUrl).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + undefined, + ) expect(then.result).toEqual({ url }) }) }) - describe('file not stored in AWS S3', () => { - const caseId = uuid() - const fileId = uuid() - const caseFile = { id: fileId } as CaseFile - const theCase = {} as Case - let then: Then - - beforeEach(async () => { - then = await givenWhenThen(caseId, theCase, fileId, caseFile) - }) - - it('should throw not found exceptoin', () => { - expect(then.error).toBeInstanceOf(NotFoundException) - expect(then.error.message).toBe(`File ${fileId} does not exist in AWS S3`) - }) - }) - describe('file not found in AWS S3', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case + const theCase = { + id: caseId, + type: CaseType.INDICTMENT, + state: CaseState.DRAFT, + } as Case let mockUpdate: jest.Mock let then: Then @@ -147,44 +111,20 @@ describe('FileController - Get case file signed url', () => { then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) - it('should remove the key', () => { + it('should remove the key and throw', () => { expect(mockUpdate).toHaveBeenCalledWith( { key: null }, { where: { id: fileId } }, ) - }) - - it('should throw not found exceptoin', () => { expect(then.error).toBeInstanceOf(NotFoundException) expect(then.error.message).toBe(`File ${fileId} does not exist in AWS S3`) }) }) - describe('remote existance check fails', () => { - const caseId = uuid() - const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` - const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case - let then: Then - - beforeEach(async () => { - const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock - mockObjectExists.mockRejectedValueOnce(new Error('Some error')) - - then = await givenWhenThen(caseId, theCase, fileId, caseFile) - }) - - it('should throw error', () => { - expect(then.error).toBeInstanceOf(Error) - expect(then.error.message).toBe('Some error') - }) - }) - describe('signed url creation fails', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const theCase = {} as Case let then: Then diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/uploadCaseFileToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/uploadCaseFileToCourt.spec.ts index 9bcfe1fc294f..b249a9b8e41c 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/uploadCaseFileToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/uploadCaseFileToCourt.spec.ts @@ -6,6 +6,8 @@ import { NotFoundException } from '@nestjs/common' import { CaseFileCategory, CaseFileState, + CaseState, + CaseType, User, } from '@island.is/judicial-system/types' @@ -65,34 +67,39 @@ describe('FileController - Upload case file to court', () => { describe('AWS S3 existance check', () => { const user = {} as User const caseId = uuid() - const theCase = { id: caseId } as Case + const theCase = { + id: caseId, + type: CaseType.ELECTRONIC_DATA_DISCOVERY_INVESTIGATION, + state: CaseState.DISMISSED, + } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - let mockObjectExists: jest.Mock beforeEach(async () => { - mockObjectExists = mockAwsS3Service.objectExists as jest.Mock - await givenWhenThen(caseId, fileId, user, theCase, caseFile) }) it('should check if the file exists in AWS S3', () => { - expect(mockObjectExists).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + ) }) }) describe('AWS S3 get file', () => { const user = {} as User const caseId = uuid() - const theCase = { id: caseId } as Case + const type = CaseType.INDICTMENT + const state = CaseState.REJECTED + const theCase = { id: caseId, type, state } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - let mockGetObject: jest.Mock beforeEach(async () => { - mockGetObject = mockAwsS3Service.getObject as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(true) @@ -100,7 +107,7 @@ describe('FileController - Upload case file to court', () => { }) it('should get the file from AWS S3', () => { - expect(mockGetObject).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(type, state, key) }) }) @@ -115,7 +122,7 @@ describe('FileController - Upload case file to court', () => { courtCaseNumber, } as Case const fileId = uuid() - const key = `indictments/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const fileName = 'test.txt' const fileType = 'text/plain' const caseFile = { @@ -174,7 +181,7 @@ describe('FileController - Upload case file to court', () => { courtCaseNumber, } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const fileName = 'test.txt' const fileType = 'text/plain' const caseFile = { @@ -218,14 +225,12 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') const documentId = uuid() - let mockUpdate: jest.Mock beforeEach(async () => { - mockUpdate = mockFileModel.update as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(true) const mockGetObject = mockAwsS3Service.getObject as jest.Mock @@ -237,7 +242,7 @@ describe('FileController - Upload case file to court', () => { }) it('should update case file state', () => { - expect(mockUpdate).toHaveBeenCalledWith( + expect(mockFileModel.update).toHaveBeenCalledWith( { state: CaseFileState.STORED_IN_COURT }, { where: { id: fileId } }, ) @@ -249,7 +254,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then @@ -261,8 +266,6 @@ describe('FileController - Upload case file to court', () => { mockGetObject.mockResolvedValueOnce(content) const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1]) - const mockDeleteObject = mockAwsS3Service.deleteObject as jest.Mock - mockDeleteObject.mockResolvedValueOnce(true) then = await givenWhenThen(caseId, fileId, user, theCase, caseFile) }) @@ -277,7 +280,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then @@ -341,13 +344,11 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - let mockUpdate: jest.Mock let then: Then beforeEach(async () => { - mockUpdate = mockFileModel.update as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(false) @@ -355,7 +356,7 @@ describe('FileController - Upload case file to court', () => { }) it('should remove the key', () => { - expect(mockUpdate).toHaveBeenCalledWith( + expect(mockFileModel.update).toHaveBeenCalledWith( { key: null }, { where: { id: fileId } }, ) @@ -372,7 +373,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile let then: Then @@ -394,7 +395,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile let then: Then @@ -418,7 +419,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then @@ -445,7 +446,7 @@ describe('FileController - Upload case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then diff --git a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/archiveCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/archiveCaseFile.spec.ts index 6bef94ad98b2..a5b812d91173 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/archiveCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/archiveCaseFile.spec.ts @@ -1,5 +1,7 @@ import { uuid } from 'uuidv4' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingFileModule } from '../createTestingFileModule' import { AwsS3Service } from '../../../aws-s3' @@ -21,15 +23,13 @@ type GivenWhenThen = ( describe('InternalFileController - Archive case files', () => { let mockAwsS3Service: AwsS3Service - let mockFileModel: typeof CaseFile let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { awsS3Service, fileModel, internalFileController } = + const { awsS3Service, internalFileController } = await createTestingFileModule() mockAwsS3Service = awsS3Service - mockFileModel = fileModel givenWhenThen = async ( caseId: string, @@ -50,13 +50,15 @@ describe('InternalFileController - Archive case files', () => { describe('case file delivered', () => { const caseId = uuid() + const caseType = CaseType.INDICTMENT + const caseState = CaseState.COMPLETED const fileId = uuid() const surrogateKey = uuid() - const key = `indictments/${caseId}/${surrogateKey}/test.txt` + const key = `${caseId}/${surrogateKey}/test.txt` const newKey = `indictments/completed/${caseId}/${surrogateKey}/test.txt` const fileName = 'test.txt' const fileType = 'text/plain' - const theCase = {} as Case + const theCase = { id: caseId, type: caseType, state: caseState } as Case const caseFile = { id: fileId, caseId, @@ -67,32 +69,18 @@ describe('InternalFileController - Archive case files', () => { let then: Then beforeEach(async () => { - const mockCopyObject = mockAwsS3Service.copyObject as jest.Mock - mockCopyObject.mockResolvedValueOnce(newKey) - const mockUpdate = mockFileModel.update as jest.Mock - mockUpdate.mockResolvedValueOnce([1]) - const mockDeleteObject = mockAwsS3Service.deleteObject as jest.Mock - mockDeleteObject.mockResolvedValueOnce(true) + const mockArchiveObject = mockAwsS3Service.archiveObject as jest.Mock + mockArchiveObject.mockResolvedValueOnce(newKey) then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) - it('should copy the file to archive bucket in AWS S3', () => { - expect(mockAwsS3Service.copyObject).toHaveBeenCalledWith(key, newKey) - }) - - it('should update case file state', () => { - expect(mockFileModel.update).toHaveBeenCalledWith( - { key: newKey }, - { where: { id: fileId } }, + it('should archive the case file', () => { + expect(mockAwsS3Service.archiveObject).toHaveBeenCalledWith( + caseType, + caseState, + key, ) - }) - - it('should try to delete the file from AWS S3', () => { - expect(mockAwsS3Service.deleteObject).toHaveBeenCalledWith(key) - }) - - it('should return success', () => { expect(then.result).toEqual({ delivered: true }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourt.spec.ts index 67adfa0eec0b..6931ead61cb2 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourt.spec.ts @@ -6,6 +6,8 @@ import { NotFoundException } from '@nestjs/common' import { CaseFileCategory, CaseFileState, + CaseState, + CaseType, User, } from '@island.is/judicial-system/types' @@ -65,15 +67,19 @@ describe('InternalFileController - Deliver case file to court', () => { describe('case file delivered', () => { const caseId = uuid() + const caseType = CaseType.CUSTODY + const caseState = CaseState.RECEIVED const courtId = uuid() const courtCaseNumber = 'R-999/2021' const theCase = { id: caseId, + type: caseType, + state: caseState, courtId, courtCaseNumber, } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const fileName = 'test.txt' const fileType = 'text/plain' const caseFile = { @@ -100,11 +106,19 @@ describe('InternalFileController - Deliver case file to court', () => { }) it('should check if the file exists in AWS S3', () => { - expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith( + caseType, + caseState, + key, + ) }) it('should get the file from AWS S3', () => { - expect(mockAwsS3Service.getObject).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.getObject).toHaveBeenCalledWith( + caseType, + caseState, + key, + ) }) it('should upload the file to court', () => { @@ -159,7 +173,7 @@ describe('InternalFileController - Deliver case file to court', () => { courtCaseNumber, } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const fileName = 'test.txt' const fileType = 'text/plain' const caseFile = { @@ -170,10 +184,8 @@ describe('InternalFileController - Deliver case file to court', () => { category: caseFileCategory, } as CaseFile const content = Buffer.from('Test content') - let mockCreateDocument: jest.Mock beforeEach(async () => { - mockCreateDocument = mockCourtService.createDocument as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(true) const mockGetObject = mockAwsS3Service.getObject as jest.Mock @@ -183,7 +195,7 @@ describe('InternalFileController - Deliver case file to court', () => { }) it('should upload the file to court', () => { - expect(mockCreateDocument).toHaveBeenCalledWith( + expect(mockCourtService.createDocument).toHaveBeenCalledWith( user, caseId, courtId, @@ -202,7 +214,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then @@ -263,7 +275,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile let then: Then @@ -291,7 +303,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile let then: Then @@ -312,7 +324,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile let then: Then @@ -335,7 +347,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then @@ -361,7 +373,7 @@ describe('InternalFileController - Deliver case file to court', () => { const caseId = uuid() const theCase = { id: caseId } as Case const fileId = uuid() - const key = `uploads/${caseId}/${uuid()}/test.txt` + const key = `${caseId}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const content = Buffer.from('Test content') let then: Then diff --git a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts index 61a1a636af76..c20557346a9e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts @@ -4,6 +4,7 @@ import { type ConfigType } from '@island.is/nest/config' import { CaseFileCategory, + CaseState, CaseType, User, } from '@island.is/judicial-system/types' @@ -47,6 +48,7 @@ describe('InternalFileController - Deliver case file to court of appeals', () => const theCase = { id: caseId, type: CaseType.CUSTODY, + state: CaseState.ACCEPTED, appealCaseNumber, caseFiles: [caseFile], } as Case @@ -98,8 +100,14 @@ describe('InternalFileController - Deliver case file to court of appeals', () => }) it('should return success', () => { - expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + ) expect(mockAwsS3Service.getSignedUrl).toHaveBeenCalledWith( + theCase.type, + theCase.state, key, mockFileConfig.robotS3TimeToLiveGet, ) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFile.spec.ts index 2866c817baee..448b6983a033 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFile.spec.ts @@ -68,7 +68,7 @@ describe('limitedAccessFileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.DEFENDANT_APPEAL_CASE_FILE, } @@ -76,7 +76,7 @@ describe('limitedAccessFileController - Create case file', () => { const timeStamp = randomDate() const caseFile = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.DEFENDANT_APPEAL_CASE_FILE, id: fileId, @@ -96,7 +96,7 @@ describe('limitedAccessFileController - Create case file', () => { expect(mockFileModel.create).toHaveBeenCalledWith({ type: 'text/plain', state: CaseFileState.STORED_IN_RVG, - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, category: CaseFileCategory.DEFENDANT_APPEAL_CASE_FILE, caseId, @@ -122,14 +122,14 @@ describe('limitedAccessFileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } const fileId = uuid() const timeStamp = randomDate() const caseFile = { type: 'text/plain', - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, id: fileId, created: timeStamp, @@ -148,7 +148,7 @@ describe('limitedAccessFileController - Create case file', () => { expect(mockFileModel.create).toHaveBeenCalledWith({ type: 'text/plain', state: CaseFileState.STORED_IN_RVG, - key: `indictments/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, caseId, name: 'test.txt', @@ -164,7 +164,7 @@ describe('limitedAccessFileController - Create case file', () => { const uuId = `-${uuid()}` const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } let then: Then @@ -176,7 +176,7 @@ describe('limitedAccessFileController - Create case file', () => { it('should throw bad gateway exception', () => { expect(then.error).toBeInstanceOf(BadRequestException) expect(then.error.message).toBe( - `uploads/${caseId}/${uuId}/test.txt is not a valid key for case ${caseId}`, + `${caseId}/${uuId}/test.txt is not a valid key for case ${caseId}`, ) }) }) @@ -187,7 +187,7 @@ describe('limitedAccessFileController - Create case file', () => { const uuId = uuid() const createCaseFile: CreateFileDto = { type: 'text/plain', - key: `uploads/${caseId}/${uuId}/test.txt`, + key: `${caseId}/${uuId}/test.txt`, size: 99, } let then: Then diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFileGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFileGuards.spec.ts index a3325a93e8d8..b18200a5cb3e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFileGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createCaseFileGuards.spec.ts @@ -3,7 +3,11 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { CaseTypeGuard, CaseWriteGuard } from '../../../case' +import { + CaseCompletedGuard, + CaseTypeGuard, + CaseWriteGuard, +} from '../../../case' import { LimitedAccessWriteCaseFileGuard } from '../../guards/limitedAccessWriteCaseFile.guard' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' @@ -19,12 +23,13 @@ describe('LimitedAccessFileController - Create case file guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(3) + expect(guards).toHaveLength(4) expect(guards[0]).toBeInstanceOf(CaseTypeGuard) expect(guards[0]).toEqual({ allowedCaseTypes: [...restrictionCases, ...investigationCases], }) expect(new guards[1]()).toBeInstanceOf(CaseWriteGuard) - expect(new guards[2]()).toBeInstanceOf(LimitedAccessWriteCaseFileGuard) + expect(new guards[2]()).toBeInstanceOf(CaseCompletedGuard) + expect(new guards[3]()).toBeInstanceOf(LimitedAccessWriteCaseFileGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPost.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPost.spec.ts index c01704b5b465..ef3d724685b4 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPost.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPost.spec.ts @@ -1,6 +1,7 @@ import { uuid } from 'uuidv4' import { + CaseState, indictmentCases, investigationCases, restrictionCases, @@ -54,7 +55,7 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () 'presigned post created for %s case', (type) => { const caseId = uuid() - const theCase = { id: caseId, type } as Case + const theCase = { id: caseId, type, state: CaseState.DRAFT } as Case const createPresignedPost: CreatePresignedPostDto = { fileName: 'test.txt', type: 'text/plain', @@ -64,11 +65,11 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () beforeEach(async () => { const mockCreatePresignedPost = mockAwsS3Service.createPresignedPost as jest.Mock - mockCreatePresignedPost.mockImplementationOnce((key: string) => + mockCreatePresignedPost.mockImplementationOnce((_1, _2, key: string) => Promise.resolve({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { - key, + key: `uploads/${key}`, bucket: 'island-is-dev-upload-judicial-system', 'X-Amz-Algorithm': 'Some Algorithm', 'X-Amz-Credential': 'Some Credentials', @@ -83,16 +84,14 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () then = await givenWhenThen(caseId, createPresignedPost, theCase) }) - it('should request a presigned post from AWS S3', () => { + it('should return a presigned post', () => { expect(mockAwsS3Service.createPresignedPost).toHaveBeenCalledWith( - expect.stringMatching( - new RegExp(`^uploads/${caseId}/.{36}/test.txt$`), - ), + type, + CaseState.DRAFT, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), 'text/plain', ) - }) - it('should return a presigned post', () => { expect(then.result).toEqual({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { @@ -105,6 +104,7 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () Policy: 'Some Policy', 'X-Amz-Signature': 'Some Signature', }, + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), }) expect(then.result.fields.key).toMatch( @@ -118,7 +118,7 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () 'presigned post created for %s case', (type) => { const caseId = uuid() - const theCase = { id: caseId, type } as Case + const theCase = { id: caseId, type, state: CaseState.DRAFT } as Case const createPresignedPost: CreatePresignedPostDto = { fileName: 'test.txt', type: 'text/plain', @@ -128,11 +128,11 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () beforeEach(async () => { const mockCreatePresignedPost = mockAwsS3Service.createPresignedPost as jest.Mock - mockCreatePresignedPost.mockImplementationOnce((key: string) => + mockCreatePresignedPost.mockImplementationOnce((_1, _2, key: string) => Promise.resolve({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { - key, + key: `indictments/${key}`, bucket: 'island-is-dev-upload-judicial-system', 'X-Amz-Algorithm': 'Some Algorithm', 'X-Amz-Credential': 'Some Credentials', @@ -147,16 +147,14 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () then = await givenWhenThen(caseId, createPresignedPost, theCase) }) - it('should request a presigned post from AWS S3', () => { + it('should return a presigned post', () => { expect(mockAwsS3Service.createPresignedPost).toHaveBeenCalledWith( - expect.stringMatching( - new RegExp(`^indictments/${caseId}/.{36}/test.txt$`), - ), + type, + CaseState.DRAFT, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), 'text/plain', ) - }) - it('should return a presigned post', () => { expect(then.result).toEqual({ url: 'https://s3.eu-west-1.amazonaws.com/island-is-dev-upload-judicial-system', fields: { @@ -169,6 +167,7 @@ describe('LimitedAccesslimitedAccessFileController - Create presigned post', () Policy: 'Some Policy', 'X-Amz-Signature': 'Some Signature', }, + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), }) expect(then.result.fields.key).toMatch( diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts index b842f3d9592f..6483025331da 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts @@ -3,7 +3,11 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { CaseTypeGuard, CaseWriteGuard } from '../../../case' +import { + CaseCompletedGuard, + CaseTypeGuard, + CaseWriteGuard, +} from '../../../case' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' describe('LimitedAccessFileController - Create presigned post guards', () => { @@ -18,11 +22,12 @@ describe('LimitedAccessFileController - Create presigned post guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(2) + expect(guards).toHaveLength(3) expect(guards[0]).toBeInstanceOf(CaseTypeGuard) expect(guards[0]).toEqual({ allowedCaseTypes: [...restrictionCases, ...investigationCases], }) expect(new guards[1]()).toBeInstanceOf(CaseWriteGuard) + expect(new guards[2]()).toBeInstanceOf(CaseCompletedGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFile.spec.ts index 8bc6e5f64d3d..bcfd348c53a3 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFile.spec.ts @@ -1,10 +1,15 @@ import { uuid } from 'uuidv4' -import { CaseFileState } from '@island.is/judicial-system/types' +import { + CaseFileState, + CaseState, + CaseType, +} from '@island.is/judicial-system/types' import { createTestingFileModule } from '../createTestingFileModule' import { AwsS3Service } from '../../../aws-s3' +import { Case } from '../../../case' import { DeleteFileResponse } from '../../models/deleteFile.response' import { CaseFile } from '../../models/file.model' @@ -15,6 +20,7 @@ interface Then { type GivenWhenThen = ( caseId: string, + theCase: Case, fileId: string, casefile: CaseFile, ) => Promise @@ -36,13 +42,14 @@ describe('LimitedAccessFileController - Delete case file', () => { givenWhenThen = async ( caseId: string, + theCase: Case, fileId: string, caseFile: CaseFile, ): Promise => { const then = {} as Then await limitedAccessFileController - .deleteCaseFile(caseId, fileId, caseFile) + .deleteCaseFile(caseId, theCase, fileId, caseFile) .then((result) => (then.result = result)) .catch((error) => (then.error = error)) @@ -50,13 +57,21 @@ describe('LimitedAccessFileController - Delete case file', () => { } }) - describe('database update', () => { + describe('case file deleted', () => { const caseId = uuid() + const caseType = CaseType.RESTRAINING_ORDER + const caseState = CaseState.DRAFT + const theCase = { id: caseId, type: caseType, state: caseState } as Case const fileId = uuid() - const caseFile = { id: fileId } as CaseFile + const key = `${uuid()}/${uuid()}/test.txt` + const caseFile = { id: fileId, key } as CaseFile + let then: Then beforeEach(async () => { - await givenWhenThen(caseId, fileId, caseFile) + const mockUpdate = mockFileModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1]) + + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should update the case file status in the database', () => { @@ -64,29 +79,20 @@ describe('LimitedAccessFileController - Delete case file', () => { { state: CaseFileState.DELETED, key: null }, { where: { id: fileId } }, ) - }) - }) - - describe('AWS S3 removal', () => { - const caseId = uuid() - const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` - const caseFile = { id: fileId, key } as CaseFile - - beforeEach(async () => { - const mockUpdate = mockFileModel.update as jest.Mock - mockUpdate.mockResolvedValueOnce([1]) - - await givenWhenThen(caseId, fileId, caseFile) - }) - - it('should attempt to remove from AWS S3', () => { - expect(mockAwsS3Service.deleteObject).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.deleteObject).toHaveBeenCalledWith( + caseType, + caseState, + key, + ) + expect(then.result).toEqual({ success: true }) }) }) describe('AWS S3 removal skipped', () => { const caseId = uuid() + const caseType = CaseType.CUSTODY + const caseSate = CaseState.SUBMITTED + const theCase = { id: caseId, type: caseType, state: caseSate } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile @@ -94,7 +100,7 @@ describe('LimitedAccessFileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1]) - await givenWhenThen(caseId, fileId, caseFile) + await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should not attempt to remove from AWS S3', () => { @@ -102,26 +108,9 @@ describe('LimitedAccessFileController - Delete case file', () => { }) }) - describe('case file deleted', () => { - const caseId = uuid() - const fileId = uuid() - const caseFile = { id: fileId } as CaseFile - let then: Then - - beforeEach(async () => { - const mockUpdate = mockFileModel.update as jest.Mock - mockUpdate.mockResolvedValueOnce([1]) - - then = await givenWhenThen(caseId, fileId, caseFile) - }) - - it('should return success', () => { - expect(then.result).toEqual({ success: true }) - }) - }) - describe('case file not deleted', () => { const caseId = uuid() + const theCase = { id: caseId } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile let then: Then @@ -130,7 +119,7 @@ describe('LimitedAccessFileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([0]) - then = await givenWhenThen(caseId, fileId, caseFile) + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should return failure', () => { @@ -140,6 +129,7 @@ describe('LimitedAccessFileController - Delete case file', () => { describe('database update fails', () => { const caseId = uuid() + const theCase = { id: caseId } as Case const fileId = uuid() const caseFile = { id: fileId } as CaseFile let then: Then @@ -148,7 +138,7 @@ describe('LimitedAccessFileController - Delete case file', () => { const mockUpdate = mockFileModel.update as jest.Mock mockUpdate.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, fileId, caseFile) + then = await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should throw error', () => { diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFileGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFileGuards.spec.ts index 08fc526a564e..fb1e6b7eafb9 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFileGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/deleteCaseFileGuards.spec.ts @@ -3,7 +3,11 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { CaseTypeGuard, CaseWriteGuard } from '../../../case' +import { + CaseCompletedGuard, + CaseTypeGuard, + CaseWriteGuard, +} from '../../../case' import { CaseFileExistsGuard } from '../../guards/caseFileExists.guard' import { LimitedAccessWriteCaseFileGuard } from '../../guards/limitedAccessWriteCaseFile.guard' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' @@ -20,13 +24,14 @@ describe('LimitedAccessFileController - Delete case file guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(4) + expect(guards).toHaveLength(5) expect(guards[0]).toBeInstanceOf(CaseTypeGuard) expect(guards[0]).toEqual({ allowedCaseTypes: [...restrictionCases, ...investigationCases], }) expect(new guards[1]()).toBeInstanceOf(CaseWriteGuard) - expect(new guards[2]()).toBeInstanceOf(CaseFileExistsGuard) - expect(new guards[3]()).toBeInstanceOf(LimitedAccessWriteCaseFileGuard) + expect(new guards[2]()).toBeInstanceOf(CaseCompletedGuard) + expect(new guards[3]()).toBeInstanceOf(CaseFileExistsGuard) + expect(new guards[4]()).toBeInstanceOf(LimitedAccessWriteCaseFileGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts index 90d0ae806cb9..6f6dbc7675c8 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts @@ -2,6 +2,8 @@ import { uuid } from 'uuidv4' import { NotFoundException } from '@nestjs/common' +import { CaseState, CaseType } from '@island.is/judicial-system/types' + import { createTestingFileModule } from '../createTestingFileModule' import { AwsS3Service } from '../../../aws-s3' @@ -53,33 +55,39 @@ describe('LimitedAccessFileController - Get case file signed url', () => { describe('AWS S3 existance check', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case - let mockObjectExists: jest.Mock + const theCase = { + id: caseId, + type: CaseType.INTERNET_USAGE, + state: CaseState.ACCEPTED, + } as Case beforeEach(async () => { - mockObjectExists = mockAwsS3Service.objectExists as jest.Mock - await givenWhenThen(caseId, theCase, fileId, caseFile) }) it('should check if the file exists in AWS S3', () => { - expect(mockObjectExists).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.objectExists).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + ) }) }) describe('AWS S3 get signed url', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile - const theCase = {} as Case - - let mockGetSignedUrl: jest.Mock + const theCase = { + id: uuid(), + type: CaseType.PHONE_TAPPING, + state: CaseState.SUBMITTED, + } as Case beforeEach(async () => { - mockGetSignedUrl = mockAwsS3Service.getSignedUrl as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(true) @@ -87,14 +95,19 @@ describe('LimitedAccessFileController - Get case file signed url', () => { }) it('should get signed url from AWS S3', () => { - expect(mockGetSignedUrl).toHaveBeenCalledWith(key) + expect(mockAwsS3Service.getSignedUrl).toHaveBeenCalledWith( + theCase.type, + theCase.state, + key, + undefined, + ) }) }) describe('signed url created', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const theCase = {} as Case @@ -136,15 +149,12 @@ describe('LimitedAccessFileController - Get case file signed url', () => { describe('file not found in AWS S3', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const theCase = {} as Case - - let mockUpdate: jest.Mock let then: Then beforeEach(async () => { - mockUpdate = mockFileModel.update as jest.Mock const mockObjectExists = mockAwsS3Service.objectExists as jest.Mock mockObjectExists.mockResolvedValueOnce(false) @@ -152,7 +162,7 @@ describe('LimitedAccessFileController - Get case file signed url', () => { }) it('should remove the key', () => { - expect(mockUpdate).toHaveBeenCalledWith( + expect(mockFileModel.update).toHaveBeenCalledWith( { key: null }, { where: { id: fileId } }, ) @@ -167,7 +177,7 @@ describe('LimitedAccessFileController - Get case file signed url', () => { describe('remote existance check fails', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const theCase = {} as Case @@ -189,7 +199,7 @@ describe('LimitedAccessFileController - Get case file signed url', () => { describe('signed url creation fails', () => { const caseId = uuid() const fileId = uuid() - const key = `uploads/${uuid()}/${uuid()}/test.txt` + const key = `${uuid()}/${uuid()}/test.txt` const caseFile = { id: fileId, key } as CaseFile const theCase = {} as Case diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/limitedAccessFileControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/limitedAccessFileControllerGuards.spec.ts index 27731303484a..f38b68b9a607 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/limitedAccessFileControllerGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/limitedAccessFileControllerGuards.spec.ts @@ -1,6 +1,6 @@ import { JwtAuthGuard, RolesGuard } from '@island.is/judicial-system/auth' -import { CaseCompletedGuard, LimitedAccessCaseExistsGuard } from '../../../case' +import { LimitedAccessCaseExistsGuard } from '../../../case' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' describe('LimitedAccessFileController - guards', () => { @@ -12,10 +12,9 @@ describe('LimitedAccessFileController - guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(4) + expect(guards).toHaveLength(3) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) expect(new guards[1]()).toBeInstanceOf(RolesGuard) expect(new guards[2]()).toBeInstanceOf(LimitedAccessCaseExistsGuard) - expect(new guards[3]()).toBeInstanceOf(CaseCompletedGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/institution/test/getAll.spec.ts b/apps/judicial-system/backend/src/app/modules/institution/test/getAll.spec.ts index 7b06f2966219..9cdcd62bfd50 100644 --- a/apps/judicial-system/backend/src/app/modules/institution/test/getAll.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/institution/test/getAll.spec.ts @@ -39,7 +39,7 @@ describe('InstitutionController - Get all', () => { beforeEach(async () => { const mockFindAll = mockInstitutionModel.findAll as jest.Mock - mockFindAll.mockReturnValueOnce(institutions) + mockFindAll.mockResolvedValueOnce(institutions) then = await givenWhenThen() }) diff --git a/apps/judicial-system/backend/src/app/modules/police/index.ts b/apps/judicial-system/backend/src/app/modules/police/index.ts index 4540547920fd..6f0613c334bc 100644 --- a/apps/judicial-system/backend/src/app/modules/police/index.ts +++ b/apps/judicial-system/backend/src/app/modules/police/index.ts @@ -1 +1,5 @@ -export { CourtDocumentType, PoliceService } from './police.service' +export { + PoliceDocumentType, + PoliceDocument, + PoliceService, +} from './police.service' diff --git a/apps/judicial-system/backend/src/app/modules/police/police.controller.ts b/apps/judicial-system/backend/src/app/modules/police/police.controller.ts index 2d6321e6bc14..ff672881afd0 100644 --- a/apps/judicial-system/backend/src/app/modules/police/police.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/police/police.controller.ts @@ -106,6 +106,7 @@ export class PoliceController { return this.policeService.uploadPoliceCaseFile( caseId, theCase.type, + theCase.state, uploadPoliceCaseFile, user, ) diff --git a/apps/judicial-system/backend/src/app/modules/police/police.service.ts b/apps/judicial-system/backend/src/app/modules/police/police.service.ts index cc828f7e13bb..5380379da208 100644 --- a/apps/judicial-system/backend/src/app/modules/police/police.service.ts +++ b/apps/judicial-system/backend/src/app/modules/police/police.service.ts @@ -22,11 +22,7 @@ import { } from '@island.is/shared/utils/server' import type { User } from '@island.is/judicial-system/types' -import { - CaseState, - CaseType, - isIndictmentCase, -} from '@island.is/judicial-system/types' +import { CaseState, CaseType } from '@island.is/judicial-system/types' import { nowFactory } from '../../factories' import { AwsS3Service } from '../aws-s3' @@ -37,7 +33,7 @@ import { PoliceCaseInfo } from './models/policeCaseInfo.model' import { UploadPoliceCaseFileResponse } from './models/uploadPoliceCaseFile.response' import { policeModuleConfig } from './police.config' -export enum CourtDocumentType { +export enum PoliceDocumentType { RVKR = 'RVKR', // Krafa RVTB = 'RVTB', // Þingbók RVUR = 'RVUR', // Úrskurður @@ -48,6 +44,11 @@ export enum CourtDocumentType { RVMG = 'RVMG', // Málsgögn } +export interface PoliceDocument { + type: PoliceDocumentType + courtDocument: string +} + const getChapter = (category?: string): number | undefined => { if (!category) { return undefined @@ -164,6 +165,7 @@ export class PoliceService { private async throttleUploadPoliceCaseFile( caseId: string, caseType: CaseType, + caseState: CaseState, uploadPoliceCaseFile: UploadPoliceCaseFileDto, user: User, ): Promise { @@ -221,11 +223,9 @@ export class PoliceService { }) }) - const key = `${ - isIndictmentCase(caseType) ? 'indictments' : 'uploads' - }/${caseId}/${uuid()}/${uploadPoliceCaseFile.name}` + const key = `${caseId}/${uuid()}/${uploadPoliceCaseFile.name}` - await this.awsS3Service.putObject(key, pdf) + await this.awsS3Service.putObject(caseType, caseState, key, pdf) return { key, size: pdf.length } } @@ -416,12 +416,14 @@ export class PoliceService { async uploadPoliceCaseFile( caseId: string, caseType: CaseType, + caseState: CaseState, uploadPoliceCaseFile: UploadPoliceCaseFileDto, user: User, ): Promise { this.throttle = this.throttleUploadPoliceCaseFile( caseId, caseType, + caseState, uploadPoliceCaseFile, user, ) @@ -439,7 +441,7 @@ export class PoliceService { defendantNationalId: string, validToDate: Date, caseConclusion: string, - courtDocuments: { type: CourtDocumentType; courtDocument: string }[], + courtDocuments: PoliceDocument[], ): Promise { return this.fetchPoliceCaseApi( `${this.xRoadPath}/V2/UpdateRVCase/${caseId}`, diff --git a/apps/judicial-system/backend/src/app/modules/police/test/updatePoliceCase.spec.ts b/apps/judicial-system/backend/src/app/modules/police/test/updatePoliceCase.spec.ts index 961783990ed6..c538ced0596e 100644 --- a/apps/judicial-system/backend/src/app/modules/police/test/updatePoliceCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/police/test/updatePoliceCase.spec.ts @@ -14,7 +14,7 @@ import { createTestingPoliceModule } from './createTestingPoliceModule' import { randomDate } from '../../../test' import { policeModuleConfig } from '../police.config' -import { CourtDocumentType } from '../police.service' +import { PoliceDocumentType } from '../police.service' jest.mock('node-fetch') @@ -36,8 +36,8 @@ describe('PoliceController - Update Police Case', () => { const validToDate = randomDate() const caseConclusion = 'test conclusion' const courtDocuments = [ - { type: CourtDocumentType.RVKR, courtDocument: 'test request pdf' }, - { type: CourtDocumentType.RVTB, courtDocument: 'test court record pdf' }, + { type: PoliceDocumentType.RVKR, courtDocument: 'test request pdf' }, + { type: PoliceDocumentType.RVTB, courtDocument: 'test court record pdf' }, ] let mockConfig: ConfigType diff --git a/apps/judicial-system/backend/src/app/modules/police/test/uploadPoliceCaseFile.spec.ts b/apps/judicial-system/backend/src/app/modules/police/test/uploadPoliceCaseFile.spec.ts index ba21a64a5b98..771661cc29bb 100644 --- a/apps/judicial-system/backend/src/app/modules/police/test/uploadPoliceCaseFile.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/police/test/uploadPoliceCaseFile.spec.ts @@ -4,7 +4,7 @@ import { uuid } from 'uuidv4' import { BadGatewayException, NotFoundException } from '@nestjs/common' -import { CaseType, User } from '@island.is/judicial-system/types' +import { CaseState, CaseType, User } from '@island.is/judicial-system/types' import { createTestingPoliceModule } from './createTestingPoliceModule' @@ -22,6 +22,8 @@ interface Then { type GivenWhenThen = ( caseId: string, + caseType: CaseType, + caseSate: CaseState, user: User, uploadPoliceCaseFile: UploadPoliceCaseFileDto, ) => Promise @@ -35,8 +37,13 @@ describe('PoliceController - Upload police case file', () => { mockAwsS3Service = awsS3Service + const mockPutObject = mockAwsS3Service.putObject as jest.Mock + mockPutObject.mockRejectedValue(new Error('Some error')) + givenWhenThen = async ( caseId: string, + caseType: CaseType, + caseSate: CaseState, user: User, uploadPoliceCaseFile: UploadPoliceCaseFileDto, ): Promise => { @@ -44,7 +51,9 @@ describe('PoliceController - Upload police case file', () => { await policeController .uploadPoliceCaseFile(caseId, user, uploadPoliceCaseFile, { - type: CaseType.CUSTODY, + id: caseId, + type: caseType, + state: caseSate, } as Case) .then((result) => (then.result = result)) .catch((error) => (then.error = error)) @@ -53,67 +62,66 @@ describe('PoliceController - Upload police case file', () => { } }) - describe('remote police call', () => { - const caseId = uuid() - const user = {} as User - const policeFileId = uuid() - const uploadPoliceCaseFile = { - id: policeFileId, - } as UploadPoliceCaseFileDto - - beforeEach(async () => { - await givenWhenThen(caseId, user, uploadPoliceCaseFile) - }) - - it('should get the police file', () => { - expect(fetch).toHaveBeenCalledWith( - expect.stringMatching( - new RegExp(`.*/GetPDFDocumentByID/${policeFileId}`), - ), - expect.anything(), - ) - }) - }) - - describe('remote AWS S3 call', () => { + describe('file uploaded to AWS S3', () => { const caseId = uuid() - const user = {} as User + const user = { id: uuid() } as User const policeFileId = uuid() const fileName = 'test.txt' const uploadPoliceCaseFile = { id: policeFileId, name: fileName, } as UploadPoliceCaseFileDto - let mockPutObject: jest.Mock + const key = uuid() + let then: Then beforeEach(async () => { - mockPutObject = mockAwsS3Service.putObject as jest.Mock const mockFetch = fetch as jest.Mock mockFetch.mockResolvedValueOnce({ ok: true, json: () => Base64.btoa('Test content'), }) - - await givenWhenThen(caseId, user, uploadPoliceCaseFile) + const mockPutObject = mockAwsS3Service.putObject as jest.Mock + mockPutObject.mockResolvedValueOnce(key) + + then = await givenWhenThen( + caseId, + CaseType.CUSTODY, + CaseState.DRAFT, + user, + uploadPoliceCaseFile, + ) }) it('should updload the file to ASW S3', () => { - expect(mockPutObject).toHaveBeenCalledWith( - expect.stringMatching(new RegExp(`^uploads/${caseId}/.{36}/test.txt$`)), + expect(fetch).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp(`.*/GetPDFDocumentByID/${policeFileId}`), + ), + expect.anything(), + ) + expect(mockAwsS3Service.putObject).toHaveBeenCalledWith( + CaseType.CUSTODY, + CaseState.DRAFT, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), 'Test content', ) + expect(then.result).toEqual({ + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), + size: 12, + }) }) }) - describe('file uploaded to AWS S3', () => { + describe('indictment case file uploaded to AWS S3', () => { const caseId = uuid() - const user = {} as User + const user = { id: uuid() } as User const policeFileId = uuid() const fileName = 'test.txt' const uploadPoliceCaseFile = { id: policeFileId, name: fileName, } as UploadPoliceCaseFileDto + const key = uuid() let then: Then beforeEach(async () => { @@ -122,15 +130,33 @@ describe('PoliceController - Upload police case file', () => { ok: true, json: () => Base64.btoa('Test content'), }) - - then = await givenWhenThen(caseId, user, uploadPoliceCaseFile) + const mockPutObject = mockAwsS3Service.putObject as jest.Mock + mockPutObject.mockResolvedValueOnce(key) + + then = await givenWhenThen( + caseId, + CaseType.INDICTMENT, + CaseState.DRAFT, + user, + uploadPoliceCaseFile, + ) }) it('should updload the file to ASW S3', () => { - expect(then.result).toEqual({ - key: expect.stringMatching( - new RegExp(`^uploads/${caseId}/.{36}/test.txt$`), + expect(fetch).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp(`.*/GetPDFDocumentByID/${policeFileId}`), ), + expect.anything(), + ) + expect(mockAwsS3Service.putObject).toHaveBeenCalledWith( + CaseType.INDICTMENT, + CaseState.DRAFT, + expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), + 'Test content', + ) + expect(then.result).toEqual({ + key: expect.stringMatching(new RegExp(`^${caseId}/.{36}/test.txt$`)), size: 12, }) }) @@ -146,10 +172,13 @@ describe('PoliceController - Upload police case file', () => { let then: Then beforeEach(async () => { - const mockFetch = fetch as jest.Mock - mockFetch.mockRejectedValueOnce(new Error('Some error')) - - then = await givenWhenThen(caseId, user, uploadPoliceCaseFile) + then = await givenWhenThen( + caseId, + CaseType.BANKING_SECRECY_WAIVER, + CaseState.NEW, + user, + uploadPoliceCaseFile, + ) }) it('should throw bad gateway exception', () => { @@ -173,7 +202,13 @@ describe('PoliceController - Upload police case file', () => { const mockFetch = fetch as jest.Mock mockFetch.mockResolvedValueOnce({ ok: false, text: () => 'Some error' }) - then = await givenWhenThen(caseId, user, policeCaseFile) + then = await givenWhenThen( + caseId, + CaseType.SEARCH_WARRANT, + CaseState.NEW, + user, + policeCaseFile, + ) }) it('should throw not found exception', () => { @@ -199,10 +234,14 @@ describe('PoliceController - Upload police case file', () => { ok: true, json: () => Base64.btoa('Test content'), }) - const mockPutObject = mockAwsS3Service.putObject as jest.Mock - mockPutObject.mockRejectedValueOnce(new Error('Some error')) - then = await givenWhenThen(caseId, user, uploadPoliceCaseFile) + then = await givenWhenThen( + caseId, + CaseType.TELECOMMUNICATIONS, + CaseState.NEW, + user, + uploadPoliceCaseFile, + ) }) it('should throw error', () => { diff --git a/apps/judicial-system/backend/src/app/modules/user/test/create.spec.ts b/apps/judicial-system/backend/src/app/modules/user/test/create.spec.ts index b68210ae4bc1..ecd82c585d5e 100644 --- a/apps/judicial-system/backend/src/app/modules/user/test/create.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/user/test/create.spec.ts @@ -60,7 +60,7 @@ describe('UserController - Create', () => { beforeEach(async () => { const mockCreate = mockUserModel.create as jest.Mock - mockCreate.mockReturnValueOnce(user) + mockCreate.mockResolvedValueOnce(user) then = await givenWhenThen() }) diff --git a/apps/judicial-system/backend/src/app/modules/user/test/getAll.spec.ts b/apps/judicial-system/backend/src/app/modules/user/test/getAll.spec.ts index 0e917132a8d0..22061c678711 100644 --- a/apps/judicial-system/backend/src/app/modules/user/test/getAll.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/user/test/getAll.spec.ts @@ -41,7 +41,7 @@ describe('UserController - Get all', () => { beforeEach(async () => { const mockFindAll = mockUserModel.findAll as jest.Mock - mockFindAll.mockReturnValueOnce(users) + mockFindAll.mockResolvedValueOnce(users) then = await givenWhenThen(UserRole.ADMIN) }) @@ -63,7 +63,7 @@ describe('UserController - Get all', () => { beforeEach(async () => { const mockFindAll = mockUserModel.findAll as jest.Mock - mockFindAll.mockReturnValueOnce(users) + mockFindAll.mockResolvedValueOnce(users) then = await givenWhenThen(role) }) diff --git a/apps/judicial-system/backend/src/app/modules/user/test/getById.spec.ts b/apps/judicial-system/backend/src/app/modules/user/test/getById.spec.ts index e305a7b0a81b..2dc4a5bf1bce 100644 --- a/apps/judicial-system/backend/src/app/modules/user/test/getById.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/user/test/getById.spec.ts @@ -40,7 +40,7 @@ describe('UserController - Get by id', () => { beforeEach(async () => { const mockFindByPk = mockUserModel.findByPk as jest.Mock - mockFindByPk.mockReturnValueOnce(user) + mockFindByPk.mockResolvedValueOnce(user) then = await givenWhenThen() }) diff --git a/apps/judicial-system/backend/src/app/modules/user/test/getByNationalId.spec.ts b/apps/judicial-system/backend/src/app/modules/user/test/getByNationalId.spec.ts index 4cf7312774fe..ddffe11c09b7 100644 --- a/apps/judicial-system/backend/src/app/modules/user/test/getByNationalId.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/user/test/getByNationalId.spec.ts @@ -74,7 +74,7 @@ describe('UserController - Get by national id', () => { beforeEach(async () => { const mockFindOne = mockUserModel.findOne as jest.Mock - mockFindOne.mockReturnValueOnce(user) + mockFindOne.mockResolvedValueOnce(user) then = await givenWhenThen(nationalId) }) diff --git a/apps/judicial-system/backend/src/app/modules/user/test/update.spec.ts b/apps/judicial-system/backend/src/app/modules/user/test/update.spec.ts index 9cbadfc6d3dc..ff4c56e73cc6 100644 --- a/apps/judicial-system/backend/src/app/modules/user/test/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/user/test/update.spec.ts @@ -42,9 +42,9 @@ describe('UserController - Update', () => { beforeEach(async () => { const mockUpdate = mockUserModel.update as jest.Mock - mockUpdate.mockReturnValueOnce([1]) + mockUpdate.mockResolvedValueOnce([1]) const mockFindByPk = mockUserModel.findByPk as jest.Mock - mockFindByPk.mockReturnValueOnce(user) + mockFindByPk.mockResolvedValueOnce(user) then = await givenWhenThen() }) diff --git a/apps/judicial-system/digital-mailbox-api/infra/judicial-system-xrd-robot-api.ts b/apps/judicial-system/digital-mailbox-api/infra/judicial-system-xrd-robot-api.ts index 22cb399a62a3..238f1658c6d2 100644 --- a/apps/judicial-system/digital-mailbox-api/infra/judicial-system-xrd-robot-api.ts +++ b/apps/judicial-system/digital-mailbox-api/infra/judicial-system-xrd-robot-api.ts @@ -21,6 +21,7 @@ export const serviceSetup = (services: { .secrets({ ERROR_EVENT_URL: '/k8s/judicial-system/ERROR_EVENT_URL', BACKEND_ACCESS_TOKEN: '/k8s/judicial-system/BACKEND_ACCESS_TOKEN', + LAWYERS_ICELAND_API_KEY: '/k8s/judicial-system/LAWYERS_ICELAND_API_KEY', }) .liveness('/liveness') .readiness('/liveness') diff --git a/apps/judicial-system/digital-mailbox-api/src/app/app.controller.ts b/apps/judicial-system/digital-mailbox-api/src/app/app.controller.ts deleted file mode 100644 index beb6a8c26822..000000000000 --- a/apps/judicial-system/digital-mailbox-api/src/app/app.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common' -import { ApiCreatedResponse } from '@nestjs/swagger' - -import type { User } from '@island.is/auth-nest-tools' -import { CurrentUser, IdsUserGuard } from '@island.is/auth-nest-tools' -import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' - -import { CasesResponse } from './models/cases.response' -import { AppService } from './app.service' - -@Controller('api') -@UseGuards(IdsUserGuard) -export class AppController { - constructor( - private readonly appService: AppService, - @Inject(LOGGER_PROVIDER) private readonly logger: Logger, - ) {} - - @Get('test') - @ApiCreatedResponse({ type: String, description: 'Test connection' }) - async test(@CurrentUser() user: User): Promise { - this.logger.debug('Testing connection') - - return this.appService.testConnection(user.nationalId) - } - - @Get('cases') - @ApiCreatedResponse({ type: String, description: 'Get all cases' }) - async getAllCases( - @CurrentUser() user: User, - @Query() query?: { lang: string }, - ): Promise { - this.logger.debug('Getting all cases') - - return this.appService.getCases(user.nationalId, query?.lang) - } -} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/app.module.ts b/apps/judicial-system/digital-mailbox-api/src/app/app.module.ts index 3c93a751c7af..9ead4377172f 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/app.module.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/app.module.ts @@ -1,3 +1,4 @@ +import { CacheModule } from '@nestjs/cache-manager' import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' @@ -8,23 +9,32 @@ import { AuditTrailModule, auditTrailModuleConfig, } from '@island.is/judicial-system/audit-trail' +import { + LawyersModule, + lawyersModuleConfig, +} from '@island.is/judicial-system/lawyers' import environment from './environments/environment' -import { digitalMailboxModuleConfig } from './app.config' -import { AppController } from './app.controller' -import { AppService } from './app.service' +import { caseModuleConfig } from './modules/cases/case.config' +import { CaseController } from './modules/cases/case.controller' +import { CaseService } from './modules/cases/case.service' +import { DefenderController } from './modules/defenders/defender.controller' @Module({ imports: [ AuditTrailModule, + LawyersModule, + CacheModule.register({ + ttl: 60 * 5 * 1000, // 5 minutes + }), ProblemModule.forRoot({ logAllErrors: true }), ConfigModule.forRoot({ isGlobal: true, - load: [digitalMailboxModuleConfig, auditTrailModuleConfig], + load: [caseModuleConfig, auditTrailModuleConfig, lawyersModuleConfig], }), AuthModule.register(environment.auth), ], - controllers: [AppController], - providers: [AppService], + controllers: [CaseController, DefenderController], + providers: [CaseService], }) export class AppModule {} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/app.service.ts b/apps/judicial-system/digital-mailbox-api/src/app/app.service.ts deleted file mode 100644 index 0941a71390e4..000000000000 --- a/apps/judicial-system/digital-mailbox-api/src/app/app.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { BadGatewayException, Inject, Injectable } from '@nestjs/common' - -import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' -import { type ConfigType } from '@island.is/nest/config' - -import { - AuditedAction, - AuditTrailService, -} from '@island.is/judicial-system/audit-trail' -import { isCompletedCase } from '@island.is/judicial-system/types' - -import { CasesResponse } from './models/cases.response' -import { InternalCasesResponse } from './models/internalCases.response' -import { digitalMailboxModuleConfig } from './app.config' - -@Injectable() -export class AppService { - constructor( - @Inject(digitalMailboxModuleConfig.KEY) - private readonly config: ConfigType, - @Inject(LOGGER_PROVIDER) private readonly logger: Logger, - private readonly auditTrailService: AuditTrailService, - ) {} - - private async test(nationalId: string): Promise { - return `OK ${nationalId}` - } - - async testConnection(nationalId: string): Promise { - return this.test(nationalId) - } - - private format( - response: InternalCasesResponse[], - lang?: string, - ): CasesResponse[] { - return response.map((item: InternalCasesResponse) => { - const language = lang?.toLowerCase() - - return { - id: item.id, - state: { - color: isCompletedCase(item.state) ? 'purple' : 'blue', - label: - language === 'en' - ? isCompletedCase(item.state) - ? 'Completed' - : 'Active' - : isCompletedCase(item.state) - ? 'Lokið' - : 'Í vinnslu', - }, - caseNumber: - language === 'en' - ? `Case number ${item.courtCaseNumber}` - : `Málsnúmer ${item.courtCaseNumber}`, - type: language === 'en' ? 'Indictment' : 'Ákæra', - } - }) - } - - private async getAllCases( - nationalId: string, - lang?: string, - ): Promise { - return fetch(`${this.config.backendUrl}/api/internal/cases/indictments`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - authorization: `Bearer ${this.config.secretToken}`, - }, - body: JSON.stringify({ nationalId }), - }) - .then(async (res) => { - const response = await res.json() - - if (res.ok) { - return this.format(response, lang) - } - - if (res.status < 500) { - throw new BadGatewayException(response?.detail) - } - - throw response - }) - .catch((reason) => { - if (reason instanceof BadGatewayException) { - throw reason - } - - throw new BadGatewayException(reason) - }) - } - - async getCases(nationalId: string, lang?: string): Promise { - return this.auditTrailService.audit( - 'digital-mailbox-api', - AuditedAction.GET_INDICTMENTS, - this.getAllCases(nationalId, lang), - nationalId, - ) - } -} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/models/cases.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/models/cases.response.ts deleted file mode 100644 index 36b1db25739d..000000000000 --- a/apps/judicial-system/digital-mailbox-api/src/app/models/cases.response.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' - -export class CasesResponse { - @ApiProperty({ type: String }) - id!: string - - @ApiProperty({ type: String }) - caseNumber!: string - - @ApiProperty({ type: String }) - type!: string - - @ApiProperty({ type: Object }) - state!: { - color: string - label: string - } -} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/models/internalCases.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/models/internalCases.response.ts deleted file mode 100644 index 84b653596213..000000000000 --- a/apps/judicial-system/digital-mailbox-api/src/app/models/internalCases.response.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' - -import { CaseState } from '@island.is/judicial-system/types' - -export class InternalCasesResponse { - @ApiProperty({ type: String }) - id!: string - - @ApiProperty({ type: String }) - courtCaseNumber!: string - - @ApiProperty({ type: String }) - type!: string - - @ApiProperty({ type: String }) - state!: CaseState -} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/app.config.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.config.ts similarity index 74% rename from apps/judicial-system/digital-mailbox-api/src/app/app.config.ts rename to apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.config.ts index b862108e2122..d31c82da563d 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/app.config.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@island.is/nest/config' -export const digitalMailboxModuleConfig = defineConfig({ - name: 'DigitalMailboxModule', +export const caseModuleConfig = defineConfig({ + name: 'DigitalMailboxCaseModule', load: (env) => ({ backendUrl: env.required('BACKEND_URL', 'http://localhost:3344'), secretToken: env.required( diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.controller.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.controller.ts new file mode 100644 index 000000000000..59167a87832b --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.controller.ts @@ -0,0 +1,88 @@ +import { + Body, + Controller, + Get, + Inject, + Param, + Patch, + Query, + UseGuards, +} from '@nestjs/common' +import { ApiOkResponse, ApiTags } from '@nestjs/swagger' + +import type { User } from '@island.is/auth-nest-tools' +import { CurrentUser, IdsUserGuard } from '@island.is/auth-nest-tools' +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' + +import { UpdateSubpoenaDto } from './dto/subpoena.dto' +import { CaseResponse } from './models/case.response' +import { CasesResponse } from './models/cases.response' +import { SubpoenaResponse } from './models/subpoena.response' +import { CaseService } from './case.service' + +@Controller('api') +@ApiTags('cases') +@UseGuards(IdsUserGuard) +export class CaseController { + constructor( + private readonly caseService: CaseService, + + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + @Get('cases') + @ApiOkResponse({ type: String, description: 'Get all cases' }) + async getAllCases( + @CurrentUser() user: User, + @Query() query?: { lang: string }, + ): Promise { + this.logger.debug('Getting all cases') + + return this.caseService.getCases(user.nationalId, query?.lang) + } + + @Get('case/:caseId') + @ApiOkResponse({ type: CaseResponse, description: 'Get case by id' }) + async getCase( + @Param('caseId') caseId: string, + @CurrentUser() user: User, + @Query() query?: { lang: string }, + ): Promise { + this.logger.debug('Getting case by id') + + return this.caseService.getCase(caseId, user.nationalId, query?.lang) + } + + @Get('case/:caseId/subpoena') + @ApiOkResponse({ + type: () => SubpoenaResponse, + description: 'Get subpoena by case id', + }) + async getSubpoena( + @Param('caseId') caseId: string, + @CurrentUser() user: User, + ): Promise { + this.logger.debug(`Getting subpoena by case id ${caseId}`) + + return this.caseService.getSubpoena(caseId, user.nationalId) + } + + @Patch('case/:caseId/subpoena') + @ApiOkResponse({ + type: () => SubpoenaResponse, + description: 'Update subpoena info', + }) + async updateSubpoena( + @CurrentUser() user: User, + @Param('caseId') caseId: string, + @Body() defenderAssignment: UpdateSubpoenaDto, + ): Promise { + this.logger.debug(`Assigning defender to subpoena ${caseId}`) + + return this.caseService.updateSubpoena( + user.nationalId, + caseId, + defenderAssignment, + ) + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts new file mode 100644 index 000000000000..76b4083fe6fc --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts @@ -0,0 +1,269 @@ +import { + BadGatewayException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common' + +import { type ConfigType } from '@island.is/nest/config' + +import { + AuditedAction, + AuditTrailService, +} from '@island.is/judicial-system/audit-trail' +import { LawyersService } from '@island.is/judicial-system/lawyers' +import { DefenderChoice } from '@island.is/judicial-system/types' + +import { UpdateSubpoenaDto } from './dto/subpoena.dto' +import { CaseResponse } from './models/case.response' +import { CasesResponse } from './models/cases.response' +import { InternalCaseResponse } from './models/internal/internalCase.response' +import { InternalCasesResponse } from './models/internal/internalCases.response' +import { InternalDefendantResponse } from './models/internal/internalDefendant.response' +import { SubpoenaResponse } from './models/subpoena.response' +import { caseModuleConfig } from './case.config' + +@Injectable() +export class CaseService { + constructor( + @Inject(caseModuleConfig.KEY) + private readonly config: ConfigType, + private readonly auditTrailService: AuditTrailService, + private readonly lawyersService: LawyersService, + ) {} + + async getCases(nationalId: string, lang?: string): Promise { + return this.auditTrailService.audit( + 'digital-mailbox-api', + AuditedAction.GET_INDICTMENTS, + this.getCasesInfo(nationalId, lang), + nationalId, + ) + } + + async getCase( + id: string, + nationalId: string, + lang?: string, + ): Promise { + return this.auditTrailService.audit( + 'digital-mailbox-api', + AuditedAction.GET_INDICTMENT, + this.getCaseInfo(id, nationalId, lang), + () => id, + ) + } + + async getSubpoena( + caseId: string, + nationalId: string, + ): Promise { + return this.auditTrailService.audit( + 'digital-mailbox-api', + AuditedAction.GET_SUBPOENA, + this.getSubpoenaInfo(caseId, nationalId), + nationalId, + ) + } + + async updateSubpoena( + nationalId: string, + caseId: string, + updateSubpoena: UpdateSubpoenaDto, + ): Promise { + return await this.auditTrailService.audit( + 'digital-mailbox-api', + AuditedAction.ASSIGN_DEFENDER_TO_SUBPOENA, + this.updateSubpoenaDefender(nationalId, caseId, updateSubpoena), + nationalId, + ) + } + + private async getCasesInfo( + nationalId: string, + lang?: string, + ): Promise { + const response = await this.fetchCases(nationalId) + return CasesResponse.fromInternalCasesResponse(response, lang) + } + + private async getCaseInfo( + id: string, + nationalId: string, + lang?: string, + ): Promise { + const response = await this.fetchCase(id, nationalId) + return CaseResponse.fromInternalCaseResponse(response, lang) + } + + private async getSubpoenaInfo( + caseId: string, + defendantNationalId: string, + ): Promise { + const caseData = await this.fetchCase(caseId, defendantNationalId) + return SubpoenaResponse.fromInternalCaseResponse( + caseData, + defendantNationalId, + ) + } + + private async updateSubpoenaDefender( + defendantNationalId: string, + caseId: string, + defenderAssignment: UpdateSubpoenaDto, + ): Promise { + let defenderChoice = { ...defenderAssignment } + + if ( + defenderAssignment.defenderNationalId && + defenderAssignment.defenderChoice === DefenderChoice.CHOOSE + ) { + const lawyers = await this.lawyersService.getLawyers() + const chosenLawyer = lawyers.find( + (l) => l.SSN === defenderAssignment.defenderNationalId, + ) + if (!chosenLawyer) { + throw new NotFoundException('Lawyer not found') + } + + defenderChoice = { + ...defenderChoice, + ...{ + defenderName: chosenLawyer.Name, + defenderEmail: chosenLawyer.Email, + defenderPhoneNumber: chosenLawyer.Phone, + }, + } + } + + await this.patchSubpoenaDefender( + defendantNationalId, + caseId, + defenderChoice, + ) + + const updatedCase = await this.fetchCase(caseId, defendantNationalId) + + return SubpoenaResponse.fromInternalCaseResponse( + updatedCase, + defendantNationalId, + ) + } + + private async fetchCases( + nationalId: string, + ): Promise { + try { + const res = await fetch( + `${this.config.backendUrl}/api/internal/cases/indictments`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.config.secretToken}`, + }, + body: JSON.stringify({ nationalId }), + }, + ) + + if (!res.ok) { + throw new BadGatewayException( + 'Unexpected error occurred while fetching cases', + ) + } + + return await res.json() + } catch (reason) { + throw new BadGatewayException(`Failed to fetch cases: ${reason.message}`) + } + } + + private async fetchCase( + id: string, + nationalId: string, + ): Promise { + try { + const res = await fetch( + `${this.config.backendUrl}/api/internal/cases/indictment/${id}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.config.secretToken}`, + }, + body: JSON.stringify({ nationalId }), + }, + ) + + if (!res.ok) { + if (res.status === 404) { + throw new NotFoundException(`Case ${id} not found`) + } + + const reason = await res.text() + + throw new BadGatewayException( + reason || 'Unexpected error occurred while fetching case by ID', + ) + } + + return await res.json() + } catch (reason) { + if ( + reason instanceof BadGatewayException || + reason instanceof NotFoundException + ) { + throw reason + } + + throw new BadGatewayException( + `Failed to fetch case by id: ${reason.message}`, + ) + } + } + + private async patchSubpoenaDefender( + defendantNationalId: string, + caseId: string, + defenderChoice: UpdateSubpoenaDto, + ): Promise { + try { + const response = await fetch( + `${this.config.backendUrl}/api/internal/case/${caseId}/defense/${defendantNationalId}`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.config.secretToken}`, + }, + body: JSON.stringify(defenderChoice), + }, + ) + + if (!response.ok) { + const errorResponse = await response.json() + throw new BadGatewayException( + `Failed to assign defender: ${ + errorResponse.message || response.statusText + }`, + ) + } + + const updatedDefendant = + (await response.json()) as InternalDefendantResponse + + return { + id: updatedDefendant.id, + defenderChoice: updatedDefendant.defenderChoice, + defenderName: updatedDefendant.defenderName, + } as InternalDefendantResponse + } catch (error) { + if (error instanceof NotFoundException) { + throw error + } + throw new BadGatewayException( + error.message || 'An unexpected error occurred', + ) + } + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/dto/subpoena.dto.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/dto/subpoena.dto.ts new file mode 100644 index 000000000000..e8ef5b772ae9 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/dto/subpoena.dto.ts @@ -0,0 +1,17 @@ +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { DefenderChoice } from '@island.is/judicial-system/types' + +export class UpdateSubpoenaDto { + @IsNotEmpty() + @IsEnum(DefenderChoice) + @ApiProperty({ enum: DefenderChoice }) + defenderChoice!: DefenderChoice + + @IsOptional() + @IsString() + @ApiProperty({ type: String }) + defenderNationalId?: string +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts new file mode 100644 index 000000000000..122d51ba5589 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts @@ -0,0 +1,121 @@ +import { ApiProperty } from '@nestjs/swagger' + +import { InternalCaseResponse } from './internal/internalCase.response' + +class IndictmentCaseData { + @ApiProperty({ type: String }) + caseNumber!: string + + @ApiProperty({ type: Object }) + groups!: Groups[] +} + +class Groups { + @ApiProperty({ type: String }) + label!: string + + @ApiProperty({ type: Object }) + items!: Items[] +} + +class Items { + @ApiProperty({ type: String }) + label!: string + + @ApiProperty({ type: String }) + value?: string + + @ApiProperty({ type: String }) + linkType?: 'email' | 'tel' +} + +export class CaseResponse { + @ApiProperty({ type: Object }) + data!: IndictmentCaseData + + static fromInternalCaseResponse( + res: InternalCaseResponse, + lang?: string, + ): CaseResponse { + const language = lang?.toLowerCase() + const defendant = res.defendants[0] + + return { + data: { + caseNumber: + language === 'en' + ? `Case number ${res.courtCaseNumber}` + : `Málsnúmer ${res.courtCaseNumber}`, + groups: [ + { + label: language === 'en' ? 'Defendant' : 'Varnaraðili', + items: [ + [language === 'en' ? 'Name' : 'Nafn', defendant.name], + [ + language === 'en' ? 'National ID' : 'Kennitala', + defendant.nationalId, + ], + [ + language === 'en' ? 'Address' : 'Heimilisfang', + defendant.address, + ], + ].map((item) => ({ + label: item[0] ?? '', + value: item[1] ?? (language === 'en' ? 'N/A' : 'Ekki skráð'), + })), + }, + { + label: language === 'en' ? 'Defender' : 'Verjandi', + items: [ + [language === 'en' ? 'Name' : 'Nafn', defendant.defenderName], + [ + language === 'en' ? 'Email' : 'Netfang', + defendant.defenderEmail, + 'email', + ], + [ + language === 'en' ? 'Phone Nr.' : 'Símanúmer', + defendant.defenderPhoneNumber, + 'tel', + ], + ].map((item) => ({ + label: item[0] ?? '', + value: item[1] ?? (language === 'en' ? 'N/A' : 'Ekki skráð'), + linkType: item[2] ?? undefined, + })), + }, + { + label: language === 'en' ? 'Information' : 'Málsupplýsingar', + items: [ + { + label: language === 'en' ? 'Type' : 'Tegund', + value: language === 'en' ? 'Indictment' : 'Ákæra', + }, + { + label: + language === 'en' ? 'Case number' : 'Málsnúmer héraðsdóms', + value: res.courtCaseNumber, + }, + { + label: language === 'en' ? 'Court' : 'Dómstóll', + value: res.court.name, + }, + { + label: language === 'en' ? 'Judge' : 'Dómari', + value: res.judge.name, + }, + { + label: language === 'en' ? 'Institution' : 'Embætti', + value: res.prosecutorsOffice.name, + }, + { + label: language === 'en' ? 'Prosecutor' : 'Ákærandi', + value: res.prosecutor.name, + }, + ], + }, + ], + }, + } + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts new file mode 100644 index 000000000000..121a299fcbe1 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger' + +import { isCompletedCase } from '@island.is/judicial-system/types' + +import { InternalCasesResponse } from './internal/internalCases.response' + +export class CasesResponse { + @ApiProperty({ type: String }) + id!: string + + @ApiProperty({ type: String }) + caseNumber!: string + + @ApiProperty({ type: String }) + type!: string + + @ApiProperty({ type: Object }) + state!: { + color: string + label: string + } + + static fromInternalCasesResponse( + response: InternalCasesResponse[], + lang?: string, + ): CasesResponse[] { + return response.map((item: InternalCasesResponse) => { + const language = lang?.toLowerCase() + + return { + id: item.id, + state: { + color: isCompletedCase(item.state) ? 'purple' : 'blue', + label: + language === 'en' + ? isCompletedCase(item.state) + ? 'Completed' + : 'Active' + : isCompletedCase(item.state) + ? 'Lokið' + : 'Í vinnslu', + }, + caseNumber: + language === 'en' + ? `Case number ${item.courtCaseNumber}` + : `Málsnúmer ${item.courtCaseNumber}`, + type: language === 'en' ? 'Indictment' : 'Ákæra', + } + }) + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts new file mode 100644 index 000000000000..e01d1958d91a --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts @@ -0,0 +1,29 @@ +import { + DefenderChoice, + Gender, + Institution, + User, +} from '@island.is/judicial-system/types' + +export class InternalCaseResponse { + id!: string + courtCaseNumber!: string + defendants!: Defendant[] + court!: Institution + judge!: User + prosecutorsOffice!: Institution + prosecutor!: User +} + +interface Defendant { + nationalId?: string + name?: string + gender?: Gender + address?: string + citizenship?: string + defenderName?: string + defenderNationalId?: string + defenderEmail?: string + defenderPhoneNumber?: string + defenderChoice?: DefenderChoice +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCases.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCases.response.ts new file mode 100644 index 000000000000..145479becbf3 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCases.response.ts @@ -0,0 +1,8 @@ +import { CaseState } from '@island.is/judicial-system/types' + +export class InternalCasesResponse { + id!: string + courtCaseNumber!: string + type!: string + state!: CaseState +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalDefendant.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalDefendant.response.ts new file mode 100644 index 000000000000..808519a0d05c --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalDefendant.response.ts @@ -0,0 +1,7 @@ +import { DefenderChoice } from '@island.is/judicial-system/types' + +export class InternalDefendantResponse { + id!: string + defenderChoice?: DefenderChoice + defenderName?: string +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts new file mode 100644 index 000000000000..85330b5e6640 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts @@ -0,0 +1,50 @@ +import { ApiProperty } from '@nestjs/swagger' + +import { formatNationalId } from '@island.is/judicial-system/formatters' +import { DefenderChoice } from '@island.is/judicial-system/types' + +import { InternalCaseResponse } from './internal/internalCase.response' + +class DefenderInfo { + @ApiProperty({ enum: () => DefenderChoice }) + defenderChoice?: DefenderChoice + + @ApiProperty({ type: () => String }) + defenderName?: string +} + +export class SubpoenaResponse { + @ApiProperty({ type: () => String }) + caseId!: string + + @ApiProperty({ type: () => String }) + displayInfo?: string + + @ApiProperty({ type: () => DefenderInfo }) + defenderInfo?: DefenderInfo + + static fromInternalCaseResponse( + internalCase: InternalCaseResponse, + defendantNationalId: string, + lang?: string, + ): SubpoenaResponse { + const formattedNationalId = formatNationalId(defendantNationalId) + + const defendantInfo = internalCase.defendants.find( + (defendant) => + defendant.nationalId === formattedNationalId || + defendant.nationalId === defendantNationalId, + ) + + return { + caseId: internalCase.id, + displayInfo: lang === 'en' ? 'Subpoena' : 'Þingbók', + defenderInfo: defendantInfo + ? { + defenderChoice: defendantInfo?.defenderChoice, + defenderName: defendantInfo?.defenderName, + } + : undefined, + } + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/defender.controller.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/defender.controller.ts new file mode 100644 index 000000000000..0b9424c51aa9 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/defender.controller.ts @@ -0,0 +1,46 @@ +import { CacheInterceptor } from '@nestjs/cache-manager' +import { + Controller, + Get, + Inject, + InternalServerErrorException, + UseInterceptors, +} from '@nestjs/common' +import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger' + +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' + +import { LawyersService } from '@island.is/judicial-system/lawyers' + +import { Defender } from './models/defender.response' + +@Controller('api') +@ApiTags('defenders') +@UseInterceptors(CacheInterceptor) +export class DefenderController { + constructor( + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + private readonly lawyersService: LawyersService, + ) {} + + @Get('defenders') + @ApiCreatedResponse({ + type: [Defender], + description: 'Retrieves a list of defenders', + }) + async getLawyers(): Promise { + try { + this.logger.debug('Retrieving lawyers from lawyer registry') + + const lawyers = await this.lawyersService.getLawyers() + return lawyers.map((lawyer) => ({ + nationalId: lawyer.SSN, + name: lawyer.Name, + practice: lawyer.Practice, + })) + } catch (error) { + this.logger.error('Failed to retrieve lawyers', error) + throw new InternalServerErrorException('Failed to retrieve lawyers') + } + } +} diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/models/defender.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/models/defender.response.ts new file mode 100644 index 000000000000..e6b8f357a2a1 --- /dev/null +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/defenders/models/defender.response.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger' + +export class Defender { + @ApiProperty({ type: String }) + nationalId!: string + + @ApiProperty({ type: String }) + name!: string + + @ApiProperty({ type: String }) + practice!: string +} diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index a38e90c4fc42..6f2df8dfaa89 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -20,7 +20,7 @@ query Case($input: CaseQueryInput!) { defenderNationalId defenderEmail defenderPhoneNumber - defendantWaivesRightToCounsel + defenderChoice defendantPlea serviceRequirement verdictViewDate diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index e54f9fba060c..7e291a374a05 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -27,7 +27,7 @@ query LimitedAccessCase($input: CaseQueryInput!) { defenderNationalId defenderEmail defenderPhoneNumber - defendantWaivesRightToCounsel + defenderChoice verdictViewDate } defenderName @@ -144,5 +144,6 @@ query LimitedAccessCase($input: CaseQueryInput!) { name } postponedIndefinitelyExplanation + indictmentRulingDecision } } diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index ec0833e1f0ef..675bfd8a3d58 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -6,6 +6,7 @@ import { Box, Text } from '@island.is/island-ui/core' import { isCompletedCase, isDistrictCourtUser, + isTrafficViolationCase, } from '@island.is/judicial-system/types' import { FileNotFoundModal, @@ -19,7 +20,6 @@ import { } from '@island.is/judicial-system-web/src/graphql/schema' import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import { useFileList } from '@island.is/judicial-system-web/src/utils/hooks' -import { isTrafficViolationIndictment } from '@island.is/judicial-system-web/src/utils/stepHelper' import { caseFiles } from '../../routes/Prosecutor/Indictments/CaseFiles/CaseFiles.strings' import { strings } from './IndictmentCaseFilesList.strings' @@ -65,8 +65,7 @@ const IndictmentCaseFilesList: React.FC> = ( caseId: workingCase.id, }) - const showTrafficViolationCaseFiles = - isTrafficViolationIndictment(workingCase) + const showTrafficViolationCaseFiles = isTrafficViolationCase(workingCase) const cf = workingCase.caseFiles @@ -79,9 +78,6 @@ const IndictmentCaseFilesList: React.FC> = ( const criminalRecords = cf?.filter( (file) => file.category === CaseFileCategory.CRIMINAL_RECORD, ) - const criminalRecordUpdates = cf?.filter( - (file) => file.category === CaseFileCategory.CRIMINAL_RECORD_UPDATE, - ) const costBreakdowns = cf?.filter( (file) => file.category === CaseFileCategory.COST_BREAKDOWN, ) diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx index 75a92e5a2259..32a9c07c9d6d 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx @@ -28,12 +28,12 @@ interface UniqueDefendersProps { } interface DataSection { - data: Array<{ title: string; value?: React.ReactNode }> + data: { title: string; value?: React.ReactNode }[] } interface Props { - courtOfAppealData?: Array<{ title: string; value?: React.ReactNode }> - data: Array<{ title: string; value?: React.ReactNode }> + courtOfAppealData?: { title: string; value?: React.ReactNode }[] + data: { title: string; value?: React.ReactNode }[] defendants?: { title: string items: Defendant[] diff --git a/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx b/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx index 3879a63f7bc0..e15880b458b2 100644 --- a/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx +++ b/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx @@ -43,6 +43,7 @@ const MobilePastCase: React.FC = ({ isCourtRole={isCourtRole} isValidToDateInThePast={theCase.isValidToDateInThePast} courtDate={theCase.courtDate} + indictmentRulingDecision={theCase.indictmentRulingDecision} />, ]} isLoading={isLoading} diff --git a/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx b/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx index c7db3878682b..64595d53231a 100644 --- a/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx +++ b/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx @@ -169,6 +169,7 @@ const PastCasesTable: React.FC> = (props) => { caseType={column.type} isCourtRole={isDistrictCourtUser(user)} isValidToDateInThePast={column.isValidToDateInThePast} + indictmentRulingDecision={column.indictmentRulingDecision} /> {column.appealState && ( diff --git a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.spec.ts b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.spec.ts index 2dce52f5335b..d742df360bc2 100644 --- a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.spec.ts +++ b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.spec.ts @@ -78,7 +78,7 @@ describe('mapCaseStateToTagVariant', () => { text: strings.active.defaultMessage, }) - expect(fn(CaseState.ACCEPTED, false, CaseType.INDICTMENT)).toEqual({ + expect(fn(CaseState.COMPLETED, false, CaseType.INDICTMENT)).toEqual({ color: 'darkerBlue', text: strings.inactive.defaultMessage, }) diff --git a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts index 2bcb77190931..23440548181d 100644 --- a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts +++ b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts @@ -67,4 +67,10 @@ export const strings = defineMessages({ defaultMessage: 'Móttekið', description: 'Notað sem merki þegar mál í stöðu "Móttekið" í málalista', }, + completed: { + id: 'judicial.system.core:tag_case_state.completed', + defaultMessage: + '{indictmentRulingDecision, select, RULING {Dómur} FINE {Viðurlagaákvörðun} DISMISSAL {Frávísun} CANCELLATION {Niðurfelling} other {Lokið}}', + description: 'Notað sem merki þegar mál í stöðu "Dómþulur" í málalista', + }, }) diff --git a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.tsx b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.tsx index e920234c4642..8cb3498d30bc 100644 --- a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.tsx +++ b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.tsx @@ -2,11 +2,9 @@ import React from 'react' import { IntlShape, useIntl } from 'react-intl' import { Tag, TagVariant } from '@island.is/island-ui/core' +import { isInvestigationCase } from '@island.is/judicial-system/types' import { - isIndictmentCase, - isInvestigationCase, -} from '@island.is/judicial-system/types' -import { + CaseIndictmentRulingDecision, CaseState, CaseType, User, @@ -21,6 +19,7 @@ interface Props { isValidToDateInThePast?: boolean | null courtDate?: string | null indictmentReviewer?: User | null + indictmentRulingDecision?: CaseIndictmentRulingDecision | null customMapCaseStateToTag?: ( formatMessage: IntlShape['formatMessage'], state?: CaseState | null, @@ -53,6 +52,7 @@ export const mapCaseStateToTagVariant = ( isValidToDateInThePast?: boolean | null, scheduledDate?: string | null, isCourtRole?: boolean, + indictmentRulingDecision?: CaseIndictmentRulingDecision | null, ): { color: TagVariant; text: string } => { switch (state) { case CaseState.NEW: @@ -71,8 +71,7 @@ export const mapCaseStateToTagVariant = ( case CaseState.MAIN_HEARING: return { color: 'blue', text: formatMessage(strings.reassignment) } case CaseState.ACCEPTED: - case CaseState.COMPLETED: - return isIndictmentCase(caseType) || isValidToDateInThePast + return isValidToDateInThePast ? { color: 'darkerBlue', text: formatMessage(strings.inactive) } : { color: 'blue', @@ -84,6 +83,11 @@ export const mapCaseStateToTagVariant = ( return { color: 'rose', text: formatMessage(strings.rejected) } case CaseState.DISMISSED: return { color: 'dark', text: formatMessage(strings.dismissed) } + case CaseState.COMPLETED: + return { + color: 'darkerBlue', + text: formatMessage(strings.completed, { indictmentRulingDecision }), + } default: return { color: 'white', text: formatMessage(strings.unknown) } } @@ -98,6 +102,7 @@ const TagCaseState: React.FC> = (Props) => { isValidToDateInThePast, courtDate, indictmentReviewer, + indictmentRulingDecision, customMapCaseStateToTag, } = Props @@ -110,6 +115,7 @@ const TagCaseState: React.FC> = (Props) => { isValidToDateInThePast, courtDate, isCourtRole, + indictmentRulingDecision, ) if (!tagVariant) return null diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx index b74b6ad4cb23..37e9619dec96 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx @@ -83,6 +83,13 @@ const Completed: FC = () => { [workingCase.id], ) + const isRulingOrFine = + workingCase.indictmentRulingDecision && + [ + CaseIndictmentRulingDecision.RULING, + CaseIndictmentRulingDecision.FINE, + ].includes(workingCase.indictmentRulingDecision) + const stepIsValid = () => workingCase.indictmentRulingDecision === CaseIndictmentRulingDecision.RULING ? workingCase.defendants?.every( @@ -112,30 +119,32 @@ const Completed: FC = () => { - - - - file.category === CaseFileCategory.CRIMINAL_RECORD_UPDATE, - )} - accept="application/pdf" - header={formatMessage(core.uploadBoxTitle)} - buttonLabel={formatMessage(core.uploadBoxButtonLabel)} - description={formatMessage(core.uploadBoxDescription, { - fileEndings: '.pdf', - })} - onChange={(files) => - handleCriminalRecordUpdateUpload( - files, - CaseFileCategory.CRIMINAL_RECORD_UPDATE, - ) - } - onRemove={(file) => handleRemoveFile(file)} - /> - + {isRulingOrFine && ( + + + + file.category === CaseFileCategory.CRIMINAL_RECORD_UPDATE, + )} + accept="application/pdf" + header={formatMessage(core.uploadBoxTitle)} + buttonLabel={formatMessage(core.uploadBoxButtonLabel)} + description={formatMessage(core.uploadBoxDescription, { + fileEndings: '.pdf', + })} + onChange={(files) => + handleCriminalRecordUpdateUpload( + files, + CaseFileCategory.CRIMINAL_RECORD_UPDATE, + ) + } + onRemove={(file) => handleRemoveFile(file)} + /> + + )} {workingCase.indictmentRulingDecision === CaseIndictmentRulingDecision.RULING && ( @@ -239,6 +248,7 @@ const Completed: FC = () => { { ]) const stepIsValid = () => { - if (!selectedAction) { + if (!allFilesDoneOrError) { return false } - if (selectedAction === 'REDISTRIBUTE') { - return uploadFiles.find( - (file) => file.category === CaseFileCategory.COURT_RECORD, - ) - } else if (selectedAction === 'POSTPONE') { - return ( - Boolean( + switch (selectedAction) { + case 'POSTPONE': + return Boolean( postponement?.postponedIndefinitely ? postponement.reason : courtDate?.date, - ) && allFilesDoneOrError - ) - } else if (selectedAction === 'COMPLETE') { - return selectedDecision !== undefined && allFilesDoneOrError + ) + case 'REDISTRIBUTE': + return uploadFiles.some( + (file) => + file.category === CaseFileCategory.COURT_RECORD && + file.status === 'done', + ) + case 'COMPLETE': + switch (selectedDecision) { + case CaseIndictmentRulingDecision.RULING: + case CaseIndictmentRulingDecision.DISMISSAL: + return ( + uploadFiles.some( + (file) => + file.category === CaseFileCategory.COURT_RECORD && + file.status === 'done', + ) && + uploadFiles.some( + (file) => + file.category === CaseFileCategory.RULING && + file.status === 'done', + ) + ) + case CaseIndictmentRulingDecision.FINE: + case CaseIndictmentRulingDecision.CANCELLATION: + return uploadFiles.some( + (file) => + file.category === CaseFileCategory.COURT_RECORD && + file.status === 'done', + ) + default: + return false + } + default: + return false } } @@ -191,7 +220,7 @@ const Conclusion: React.FC = () => { workingCase={workingCase} isLoading={isLoadingWorkingCase} notFound={caseNotFound} - isValid={allFilesDoneOrError} + isValid={stepIsValid()} onNavigationTo={handleNavigationTo} > @@ -317,16 +346,48 @@ const Conclusion: React.FC = () => { label={formatMessage(strings.ruling)} /> + + { + setSelectedDecision(CaseIndictmentRulingDecision.FINE) + }} + large + backgroundColor="white" + label={formatMessage(strings.fine)} + /> + + + { + setSelectedDecision(CaseIndictmentRulingDecision.DISMISSAL) + }} + large + backgroundColor="white" + label={formatMessage(strings.dismissal)} + /> + { - setSelectedDecision(CaseIndictmentRulingDecision.FINE) + setSelectedDecision(CaseIndictmentRulingDecision.CANCELLATION) }} large backgroundColor="white" - label={formatMessage(strings.fine)} + label={formatMessage(strings.cancellation)} /> @@ -335,7 +396,7 @@ const Conclusion: React.FC = () => { { /> )} - {selectedDecision === 'RULING' && ( - - - file.category === CaseFileCategory.RULING, - )} - accept="application/pdf" - header={formatMessage(strings.inputFieldLabel)} - description={formatMessage(core.uploadBoxDescription, { - fileEndings: '.pdf', - })} - buttonLabel={formatMessage(strings.uploadButtonText)} - onChange={(files) => { - handleUpload( - addUploadFiles(files, CaseFileCategory.RULING), - updateUploadFile, - ) - }} - onRemove={(file) => handleRemove(file, removeUploadFile)} - onRetry={(file) => handleRetry(file, updateUploadFile)} - /> - - )} + {selectedAction === 'COMPLETE' && + (selectedDecision === CaseIndictmentRulingDecision.RULING || + selectedDecision === CaseIndictmentRulingDecision.DISMISSAL) && ( + + + file.category === CaseFileCategory.RULING, + )} + accept="application/pdf" + header={formatMessage(strings.inputFieldLabel)} + description={formatMessage(core.uploadBoxDescription, { + fileEndings: '.pdf', + })} + buttonLabel={formatMessage(strings.uploadButtonText)} + onChange={(files) => { + handleUpload( + addUploadFiles(files, CaseFileCategory.RULING), + updateUploadFile, + ) + }} + onRemove={(file) => handleRemove(file, removeUploadFile)} + onRetry={(file) => handleRetry(file, updateUploadFile)} + /> + + )} > = (props) => { defenderPhoneNumber: defendantWaivesRightToCounsel ? '' : defendant.defenderPhoneNumber, - defendantWaivesRightToCounsel, + defenderChoice: + defendantWaivesRightToCounsel === true + ? DefenderChoice.WAIVE + : undefined, } setAndSendDefendantToServer(updateDefendantInput, setWorkingCase) @@ -79,7 +85,7 @@ const SelectDefender: React.FC> = (props) => { accused: formatMessage(core.indictmentDefendant, { gender }), }), )} - checked={Boolean(defendant.defendantWaivesRightToCounsel)} + checked={Boolean(defendant.defenderChoice === DefenderChoice.WAIVE)} onChange={(event: React.ChangeEvent) => { toggleDefendantWaivesRightToCounsel( workingCase.id, @@ -92,7 +98,7 @@ const SelectDefender: React.FC> = (props) => { /> diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.strings.ts b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.strings.ts index 86819bf38a17..b479674bf746 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.strings.ts +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.strings.ts @@ -7,32 +7,9 @@ export const strings = defineMessages({ description: 'Notaður sem titill á yfirliti ákæru þegar máli er ekki lokið.', }, - completedTitle: { - id: 'judicial.system.core:indictment_overview.completed_title', - defaultMessage: 'Máli lokið', - description: 'Notaður sem titill á yfirliti ákæru þegar máli er lokið.', - }, returnIndictmentButtonText: { id: 'judicial.system.core:indictment_overview.return_indictment_button_text', defaultMessage: 'Endursenda', description: 'Notaður sem texti á takka til að endursenda ákæru.', }, - sendToPublicProsecutorModalTitle: { - id: 'judicial.system.core:indictment_overview.send_to_public_prosecutor_modal_title', - defaultMessage: 'Mál hefur verið sent til Ríkissaksóknara', - description: - 'Notaður sem titill á staðfestingarglugga um að mál hafi verið sent til Ríkissaksóknara.', - }, - sendToPublicProsecutorModalText: { - id: 'judicial.system.core:indictment_overview.send_to_public_prosecutor_modal_text', - defaultMessage: 'Gögn hafa verið send til Ríkissaksóknara til yfirlesturs', - description: - 'Notaður sem texti í staðfestingarglugga um að mál hafi verið sent til Ríkissaksóknara.', - }, - sendToPublicProsecutorModalNextButtonText: { - id: 'judicial.system.core:indictment_overview.send_to_public_prosecutor_modal_next_button_text', - defaultMessage: 'Senda til ákæruvalds', - description: - 'Notaður sem texti á takka í staðfestingarglugga um að mál hafi verið sent til Ríkissaksóknara.', - }, }) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx index d47a9bace956..2e67664769a1 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx @@ -2,9 +2,9 @@ import React, { useCallback, useContext, useState } from 'react' import { useIntl } from 'react-intl' import { useRouter } from 'next/router' -import { Box, toast } from '@island.is/island-ui/core' +import { Box } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' -import { core, errors, titles } from '@island.is/judicial-system-web/messages' +import { core, titles } from '@island.is/judicial-system-web/messages' import { CourtCaseInfo, FormContentContainer, @@ -14,15 +14,12 @@ import { IndictmentsLawsBrokenAccordionItem, InfoCardActiveIndictment, InfoCardCaseScheduledIndictment, - InfoCardClosedIndictment, - Modal, PageHeader, PageLayout, PageTitle, useIndictmentsLawsBroken, } from '@island.is/judicial-system-web/src/components' import { CaseState } from '@island.is/judicial-system-web/src/graphql/schema' -import { useDefendants } from '@island.is/judicial-system-web/src/utils/hooks' import ReturnIndictmentModal from '../ReturnIndictmentCaseModal/ReturnIndictmentCaseModal' import { strings } from './Overview.strings' @@ -33,14 +30,10 @@ const IndictmentOverview = () => { useContext(FormContext) const { formatMessage } = useIntl() const lawsBroken = useIndictmentsLawsBroken(workingCase) - const { updateDefendant } = useDefendants() - const [modalVisible, setModalVisible] = useState< - 'RETURN_INDICTMENT' | 'SEND_TO_PUBLIC_PROSECUTOR' - >() + const [modalVisible, setModalVisible] = useState<'RETURN_INDICTMENT'>() const caseHasBeenReceivedByCourt = workingCase.state === CaseState.RECEIVED const latestDate = workingCase.courtDate ?? workingCase.arraignmentDate - const caseIsClosed = workingCase.state === CaseState.COMPLETED const handleNavigationTo = useCallback( (destination: string) => router.push(`${destination}/${workingCase.id}`), @@ -55,21 +48,9 @@ const IndictmentOverview = () => { isValid={true} onNavigationTo={handleNavigationTo} > - + - - {caseIsClosed - ? formatMessage(strings.completedTitle) - : formatMessage(strings.inProgressTitle)} - + {formatMessage(strings.inProgressTitle)} {caseHasBeenReceivedByCourt && workingCase.court && latestDate?.date && ( @@ -84,11 +65,7 @@ const IndictmentOverview = () => { )} - {caseIsClosed ? ( - - ) : ( - - )} + {lawsBroken.size > 0 && ( @@ -96,66 +73,28 @@ const IndictmentOverview = () => { )} {workingCase.caseFiles && ( - + )} - {caseIsClosed && ( - - { - const promises = workingCase.defendants - ? workingCase.defendants.map(async (defendant) => { - const updatedDefendant = await updateDefendant({ - caseId: workingCase.id, - defendantId: defendant.id, - serviceRequirement: defendant.serviceRequirement, - }) - - return updatedDefendant - }) - : [] - - const allDefendantsUpdated = await Promise.all(promises) - - if (allDefendantsUpdated.length > 0) { - setModalVisible('SEND_TO_PUBLIC_PROSECUTOR') - } else { - toast.error(formatMessage(errors.updateDefendant)) - } - }} - nextButtonText={formatMessage( - strings.sendToPublicProsecutorModalNextButtonText, - )} - nextIsDisabled={workingCase.defendants?.some( - (defendant) => !defendant.serviceRequirement, - )} - /> - - )} - {!caseIsClosed && ( - - - handleNavigationTo( - constants.INDICTMENTS_RECEPTION_AND_ASSIGNMENT_ROUTE, - ) - } - nextButtonText={formatMessage(core.continue)} - actionButtonText={formatMessage(strings.returnIndictmentButtonText)} - actionButtonColorScheme={'destructive'} - actionButtonIsDisabled={!workingCase.courtCaseNumber} - onActionButtonClick={() => setModalVisible('RETURN_INDICTMENT')} - /> - - )} + + + handleNavigationTo( + constants.INDICTMENTS_RECEPTION_AND_ASSIGNMENT_ROUTE, + ) + } + nextButtonText={formatMessage(core.continue)} + actionButtonText={formatMessage(strings.returnIndictmentButtonText)} + actionButtonColorScheme={'destructive'} + actionButtonIsDisabled={!caseHasBeenReceivedByCourt} + onActionButtonClick={() => setModalVisible('RETURN_INDICTMENT')} + /> + {modalVisible === 'RETURN_INDICTMENT' && ( { onComplete={() => router.push(constants.CASES_ROUTE)} /> )} - {modalVisible === 'SEND_TO_PUBLIC_PROSECUTOR' && ( - setModalVisible(undefined)} - /> - )} ) } diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.strings.ts b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.strings.ts index fc389fc02139..8bfd7f9404ce 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.strings.ts +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.strings.ts @@ -40,9 +40,8 @@ export const strings = defineMessages({ description: 'Notaður sem titill á staðfestingarglugga um að mál sé lokið.', }, completedCaseModalBody: { - id: 'judicial.system.core:indictments.summary.completed_case_modal_body', - defaultMessage: - 'Gögn hafa verið send á ákæranda og verjanda hafi þeim verið hlaðið upp.', + id: 'judicial.system.core:indictments.summary.completed_case_modal_body_v2', + defaultMessage: 'Gögn hafa verið send ákæranda og verjanda.', description: 'Notaður sem texti í staðfestingarglugga um að mál sé lokið.', }, }) diff --git a/apps/judicial-system/web/src/routes/CourtOfAppeal/Cases/appealdCases.graphql b/apps/judicial-system/web/src/routes/CourtOfAppeal/Cases/appealdCases.graphql index 8d869557e9a0..06656feca08e 100644 --- a/apps/judicial-system/web/src/routes/CourtOfAppeal/Cases/appealdCases.graphql +++ b/apps/judicial-system/web/src/routes/CourtOfAppeal/Cases/appealdCases.graphql @@ -25,7 +25,7 @@ query AppealedCases($input: CaseListQueryInput) { nationalId name noNationalId - defendantWaivesRightToCounsel + defenderChoice } appealedDate initialRulingDate diff --git a/apps/judicial-system/web/src/routes/CourtOfAppeal/components/CaseOverviewHeader/CaseOverviewHeader.tsx b/apps/judicial-system/web/src/routes/CourtOfAppeal/components/CaseOverviewHeader/CaseOverviewHeader.tsx index fa35c4f32bb1..194f9abe61db 100644 --- a/apps/judicial-system/web/src/routes/CourtOfAppeal/components/CaseOverviewHeader/CaseOverviewHeader.tsx +++ b/apps/judicial-system/web/src/routes/CourtOfAppeal/components/CaseOverviewHeader/CaseOverviewHeader.tsx @@ -87,8 +87,8 @@ const CaseOverviewHeader: React.FC = (props) => { )} {workingCase.appealRulingDecision && - workingCase.eventLogs && - workingCase.eventLogs.length > 0 && ( + filteredEvents && + filteredEvents.length > 0 && ( {filteredEvents?.map((event, index) => ( diff --git a/apps/judicial-system/web/src/routes/Defender/CaseOverview.tsx b/apps/judicial-system/web/src/routes/Defender/CaseOverview.tsx index 3752308f7c7e..4fcc37593d60 100644 --- a/apps/judicial-system/web/src/routes/Defender/CaseOverview.tsx +++ b/apps/judicial-system/web/src/routes/Defender/CaseOverview.tsx @@ -364,7 +364,9 @@ export const CaseOverview: React.FC> = () => { strings.confirmAppealAfterDeadlineModalSecondaryButtonText, )} onPrimaryButtonClick={() => { - router.push(`${constants.APPEAL_ROUTE}/${workingCase.id}`) + router.push( + `${constants.DEFENDER_APPEAL_ROUTE}/${workingCase.id}`, + ) }} onSecondaryButtonClick={() => { setModalVisible('NoModal') diff --git a/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx b/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx index 0e6e2b411cf6..3e44b9026c9c 100644 --- a/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx +++ b/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx @@ -158,6 +158,7 @@ export const DefenderCasesTable: React.FC> = ( caseType={column.type} isValidToDateInThePast={column.isValidToDateInThePast} courtDate={column.courtDate} + indictmentRulingDecision={column.indictmentRulingDecision} /> {column.appealState && ( diff --git a/apps/judicial-system/web/src/routes/Defender/Cases/defenderCases.graphql b/apps/judicial-system/web/src/routes/Defender/Cases/defenderCases.graphql index 230b576c0c32..df7b8ee18f12 100644 --- a/apps/judicial-system/web/src/routes/Defender/Cases/defenderCases.graphql +++ b/apps/judicial-system/web/src/routes/Defender/Cases/defenderCases.graphql @@ -22,10 +22,11 @@ query DefenderCases($input: CaseListQueryInput) { nationalId name noNationalId - defendantWaivesRightToCounsel + defenderChoice } initialRulingDate rulingDate postponedIndefinitelyExplanation + indictmentRulingDecision } } diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx index e0fd7ef80491..9e1593cde7a5 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx @@ -5,6 +5,7 @@ import router from 'next/router' import { Box, InputFileUpload, Text } from '@island.is/island-ui/core' import { fileExtensionWhitelist } from '@island.is/island-ui/core/types' import * as constants from '@island.is/judicial-system/consts' +import { isTrafficViolationCase } from '@island.is/judicial-system/types' import { titles } from '@island.is/judicial-system-web/messages' import { FormContentContainer, @@ -21,7 +22,6 @@ import { useS3Upload, useUploadFiles, } from '@island.is/judicial-system-web/src/utils/hooks' -import { isTrafficViolationIndictment } from '@island.is/judicial-system-web/src/utils/stepHelper' import * as strings from './CaseFiles.strings' @@ -40,7 +40,7 @@ const CaseFiles: React.FC> = () => { workingCase.id, ) - const isTrafficViolationCaseCheck = isTrafficViolationIndictment(workingCase) + const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase) const stepIsValid = uploadFiles.some( diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx index 52911b704d7c..21f06fa7cff9 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/router' import { Box, RadioButton, Text } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' +import { isTrafficViolationCase } from '@island.is/judicial-system/types' import { titles } from '@island.is/judicial-system-web/messages' import { BlueBox, @@ -27,7 +28,6 @@ import { useCase, useDefendants, } from '@island.is/judicial-system-web/src/utils/hooks' -import { isTrafficViolationIndictment } from '@island.is/judicial-system-web/src/utils/stepHelper' import { isProcessingStepValidIndictments } from '@island.is/judicial-system-web/src/utils/validate' import { ProsecutorSection, SelectCourt } from '../../components' @@ -41,7 +41,7 @@ const Processing: React.FC = () => { const { formatMessage } = useIntl() const { updateDefendant, updateDefendantState } = useDefendants() const router = useRouter() - const isTrafficViolationCaseCheck = isTrafficViolationIndictment(workingCase) + const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase) const handleNavigationTo = useCallback( async (destination: string) => { diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesForReview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesForReview.tsx index 197b5222d399..ac8a40e59f23 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesForReview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesForReview.tsx @@ -80,6 +80,7 @@ const CasesForReview: React.FC = ({ mapIndictmentCaseStateToTagVariant } indictmentReviewer={row.indictmentReviewer} + indictmentRulingDecision={row.indictmentRulingDecision} /> ), }, diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx index 1c52098aa36d..7f2a9d6253e7 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx @@ -16,6 +16,7 @@ import { import { isDistrictCourtUser, isProsecutionUser, + isRequestCase, } from '@island.is/judicial-system/types' import { core, tables } from '@island.is/judicial-system-web/messages' import { @@ -326,6 +327,7 @@ const ActiveCases: React.FC> = (props) => { isCourtRole={isDistrictCourtUser(user)} isValidToDateInThePast={c.isValidToDateInThePast} courtDate={c.courtDate} + indictmentRulingDecision={c.indictmentRulingDecision} /> {c.appealState && ( @@ -381,7 +383,10 @@ const ActiveCases: React.FC> = (props) => { onClick: () => handleOpenCase(c.id, true), icon: 'open', }, - ...(isProsecutionUser(user) + ...(isProsecutionUser(user) && + (isRequestCase(c.type) || + c.state === CaseState.DRAFT || + c.state === CaseState.WAITING_FOR_CONFIRMATION) ? [ { title: formatMessage( diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/MobileCase.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/MobileCase.tsx index fe5104b2d64d..7ebc39545481 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/MobileCase.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/MobileCase.tsx @@ -78,6 +78,7 @@ const MobileCase: React.FC> = ({ isCourtRole={isCourtRole} isValidToDateInThePast={theCase.isValidToDateInThePast} courtDate={theCase.courtDate} + indictmentRulingDecision={theCase.indictmentRulingDecision} />, ]} isLoading={isLoading} diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql index 2fd62e99ff51..0fe071daaaa2 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql @@ -25,7 +25,7 @@ query Cases { nationalId name noNationalId - defendantWaivesRightToCounsel + defenderChoice verdictViewDate } courtDate @@ -92,5 +92,6 @@ query Cases { indictmentAppealDeadline indictmentVerdictViewedByAll indictmentVerdictAppealDeadline + indictmentRulingDecision } } diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql b/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql index 7466a685cf10..a7f734cc0b13 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql @@ -25,7 +25,7 @@ query PrisonCases { nationalId name noNationalId - defendantWaivesRightToCounsel + defenderChoice } courtDate isValidToDateInThePast @@ -81,5 +81,6 @@ query PrisonCases { active } postponedIndefinitelyExplanation + indictmentRulingDecision } } diff --git a/apps/judicial-system/web/src/utils/formHelper.ts b/apps/judicial-system/web/src/utils/formHelper.ts index bc47dc522288..8cc151880683 100644 --- a/apps/judicial-system/web/src/utils/formHelper.ts +++ b/apps/judicial-system/web/src/utils/formHelper.ts @@ -190,7 +190,7 @@ export type stepValidationsType = { ) => boolean [constants.INDICTMENTS_SUBPOENA_ROUTE]: (theCase: Case) => boolean [constants.INDICTMENTS_DEFENDER_ROUTE]: (theCase: Case) => boolean - [constants.INDICTMENTS_CONCLUSION_ROUTE]: () => boolean + [constants.INDICTMENTS_CONCLUSION_ROUTE]: (theCase: Case) => boolean [constants.INDICTMENTS_COURT_OVERVIEW_ROUTE]: () => boolean [constants.INDICTMENTS_SUMMARY_ROUTE]: () => boolean [constants.COURT_OF_APPEAL_OVERVIEW_ROUTE]: () => boolean @@ -279,7 +279,8 @@ export const stepValidations = (): stepValidationsType => { validations.isSubpoenaStepValid(theCase), [constants.INDICTMENTS_DEFENDER_ROUTE]: (theCase: Case) => validations.isDefenderStepValid(theCase), - [constants.INDICTMENTS_CONCLUSION_ROUTE]: () => true, + [constants.INDICTMENTS_CONCLUSION_ROUTE]: (theCase: Case) => + validations.isConclusionStepValid(theCase), [constants.INDICTMENTS_COURT_OVERVIEW_ROUTE]: () => true, [constants.COURT_OF_APPEAL_OVERVIEW_ROUTE]: () => true, [constants.COURT_OF_APPEAL_CASE_ROUTE]: (theCase: Case) => @@ -299,15 +300,12 @@ export const findFirstInvalidStep = (steps: string[], theCase: Case) => { steps.includes(key), ) - if ( - stepsToCheck.every(([, validationFn]) => validationFn(theCase) === true) - ) { + if (stepsToCheck.every(([, validationFn]) => validationFn(theCase))) { return steps[steps.length - 1] } const [key] = - stepsToCheck.find(([, validationFn]) => validationFn(theCase) === false) || - [] + stepsToCheck.find(([, validationFn]) => !validationFn(theCase)) ?? [] return key } diff --git a/apps/judicial-system/web/src/utils/hooks/useCase/index.ts b/apps/judicial-system/web/src/utils/hooks/useCase/index.ts index 8a2d6e6e4ab0..55cea551369b 100644 --- a/apps/judicial-system/web/src/utils/hooks/useCase/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useCase/index.ts @@ -109,10 +109,7 @@ export const update = (update: UpdateCase, workingCase: Case): UpdateCase => { return validUpdates } -export const formatUpdates = ( - updates: Array, - workingCase: Case, -) => { +export const formatUpdates = (updates: UpdateCase[], workingCase: Case) => { const changes: UpdateCase[] = updates.map((entry) => { if (entry.force) { return overwrite(entry) diff --git a/apps/judicial-system/web/src/utils/hooks/useCase/limitedAccessUpdateCase.graphql b/apps/judicial-system/web/src/utils/hooks/useCase/limitedAccessUpdateCase.graphql index ac23d6071e46..8b63cf9881b4 100644 --- a/apps/judicial-system/web/src/utils/hooks/useCase/limitedAccessUpdateCase.graphql +++ b/apps/judicial-system/web/src/utils/hooks/useCase/limitedAccessUpdateCase.graphql @@ -27,7 +27,7 @@ mutation LimitedAccessUpdateCase($input: UpdateCaseInput!) { defenderNationalId defenderEmail defenderPhoneNumber - defendantWaivesRightToCounsel + defenderChoice } defenderName defenderNationalId diff --git a/apps/judicial-system/web/src/utils/hooks/useCase/updateCase.graphql b/apps/judicial-system/web/src/utils/hooks/useCase/updateCase.graphql index f9f3a850d7d6..5d88783a029f 100644 --- a/apps/judicial-system/web/src/utils/hooks/useCase/updateCase.graphql +++ b/apps/judicial-system/web/src/utils/hooks/useCase/updateCase.graphql @@ -20,7 +20,7 @@ mutation UpdateCase($input: UpdateCaseInput!) { defenderNationalId defenderEmail defenderPhoneNumber - defendantWaivesRightToCounsel + defenderChoice defendantPlea } defenderName diff --git a/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx b/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx index 6b8899dcae03..edd04e4b7511 100644 --- a/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx +++ b/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx @@ -14,10 +14,11 @@ import { isCourtOfAppealsUser, isDefenceUser, isDistrictCourtUser, - isIndictmentCase, isInvestigationCase, isPublicProsecutorUser, + isRequestCase, isRestrictionCase, + isTrafficViolationCase, } from '@island.is/judicial-system/types' import { errors } from '@island.is/judicial-system-web/messages' import { UserContext } from '@island.is/judicial-system-web/src/components' @@ -30,7 +31,6 @@ import { import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import { findFirstInvalidStep } from '../../formHelper' -import { isTrafficViolationIndictment } from '../../stepHelper' import useCase from '../useCase' const useCaseList = () => { @@ -74,60 +74,81 @@ const useCaseList = () => { const openCase = (caseToOpen: Case, user: User) => { let routeTo = null - const isTrafficViolation = isTrafficViolationIndictment(caseToOpen) + const isTrafficViolation = isTrafficViolationCase(caseToOpen) if (isDefenceUser(user)) { - if (isIndictmentCase(caseToOpen.type)) { - routeTo = DEFENDER_INDICTMENT_ROUTE - } else { + if (isRequestCase(caseToOpen.type)) { routeTo = DEFENDER_ROUTE + } else { + routeTo = DEFENDER_INDICTMENT_ROUTE } } else if (isPublicProsecutorUser(user)) { + // Public prosecutor users can only see completed indictments routeTo = constants.PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE - } else if (isCompletedCase(caseToOpen.state)) { - if (isIndictmentCase(caseToOpen.type)) { - routeTo = constants.CLOSED_INDICTMENT_OVERVIEW_ROUTE - } else if (isCourtOfAppealsUser(user)) { - if (caseToOpen.appealState === CaseAppealState.COMPLETED) { - routeTo = constants.COURT_OF_APPEAL_RESULT_ROUTE - } else { - routeTo = constants.COURT_OF_APPEAL_OVERVIEW_ROUTE - } + } else if (isCourtOfAppealsUser(user)) { + // Court of appeals users can only see appealed request cases + if (caseToOpen.appealState === CaseAppealState.COMPLETED) { + routeTo = constants.COURT_OF_APPEAL_RESULT_ROUTE } else { - routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + routeTo = constants.COURT_OF_APPEAL_OVERVIEW_ROUTE } } else if (isDistrictCourtUser(user)) { if (isRestrictionCase(caseToOpen.type)) { - routeTo = findFirstInvalidStep( - constants.courtRestrictionCasesRoutes, - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.courtRestrictionCasesRoutes, + caseToOpen, + ) + } } else if (isInvestigationCase(caseToOpen.type)) { - routeTo = findFirstInvalidStep( - constants.courtInvestigationCasesRoutes, - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.courtInvestigationCasesRoutes, + caseToOpen, + ) + } } else { - // Route to Indictment Overview section since it always a valid step and - // would be skipped if we route to the last valid step - routeTo = constants.INDICTMENTS_COURT_OVERVIEW_ROUTE + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.INDICTMENTS_COMPLETED_ROUTE + } else { + // Route to Indictment Overview section since it always a valid step and + // would be skipped if we route to the last valid step + routeTo = constants.INDICTMENTS_COURT_OVERVIEW_ROUTE + } } } else { + // The user is a prosecution user if (isRestrictionCase(caseToOpen.type)) { - routeTo = findFirstInvalidStep( - constants.prosecutorRestrictionCasesRoutes, - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorRestrictionCasesRoutes, + caseToOpen, + ) + } } else if (isInvestigationCase(caseToOpen.type)) { - routeTo = findFirstInvalidStep( - constants.prosecutorInvestigationCasesRoutes, - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorInvestigationCasesRoutes, + caseToOpen, + ) + } } else { - routeTo = findFirstInvalidStep( - constants.prosecutorIndictmentRoutes(isTrafficViolation), - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.CLOSED_INDICTMENT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorIndictmentRoutes(isTrafficViolation), + caseToOpen, + ) + } } } @@ -162,6 +183,7 @@ const useCaseList = () => { ? getLimitedAccessCase({ variables: { input: { id } } }) : getCase({ variables: { input: { id } } }) } + if ( isTransitioningCase || isSendingNotification || diff --git a/apps/judicial-system/web/src/utils/hooks/useDeb/index.tsx b/apps/judicial-system/web/src/utils/hooks/useDeb/index.tsx index 0e926219eea2..47a2352c4878 100644 --- a/apps/judicial-system/web/src/utils/hooks/useDeb/index.tsx +++ b/apps/judicial-system/web/src/utils/hooks/useDeb/index.tsx @@ -4,7 +4,7 @@ import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import useCase from '../useCase' -const useDeb = (workingCase: Case, keys: Array | keyof Case) => { +const useDeb = (workingCase: Case, keys: (keyof Case)[] | keyof Case) => { const { updateCase } = useCase() const newKeys = Array.isArray(keys) ? keys : [keys] diff --git a/apps/judicial-system/web/src/utils/hooks/useS3Upload/createPresignedPost.graphql b/apps/judicial-system/web/src/utils/hooks/useS3Upload/createPresignedPost.graphql index 7bb7c7702cea..4f3ded8599bc 100644 --- a/apps/judicial-system/web/src/utils/hooks/useS3Upload/createPresignedPost.graphql +++ b/apps/judicial-system/web/src/utils/hooks/useS3Upload/createPresignedPost.graphql @@ -2,5 +2,6 @@ mutation CreatePresignedPost($input: CreatePresignedPostInput!) { createPresignedPost(input: $input) { url fields + key } } diff --git a/apps/judicial-system/web/src/utils/hooks/useS3Upload/limitedAccessCreatePresignedPost.graphql b/apps/judicial-system/web/src/utils/hooks/useS3Upload/limitedAccessCreatePresignedPost.graphql index d578f36b3aaa..b334cc70d5ac 100644 --- a/apps/judicial-system/web/src/utils/hooks/useS3Upload/limitedAccessCreatePresignedPost.graphql +++ b/apps/judicial-system/web/src/utils/hooks/useS3Upload/limitedAccessCreatePresignedPost.graphql @@ -2,5 +2,6 @@ mutation LimitedAccessCreatePresignedPost($input: CreatePresignedPostInput!) { limitedAccessCreatePresignedPost(input: $input) { url fields + key } } diff --git a/apps/judicial-system/web/src/utils/hooks/useS3Upload/useS3Upload.ts b/apps/judicial-system/web/src/utils/hooks/useS3Upload/useS3Upload.ts index 572bc62bd1b8..9580d95bfe8a 100644 --- a/apps/judicial-system/web/src/utils/hooks/useS3Upload/useS3Upload.ts +++ b/apps/judicial-system/web/src/utils/hooks/useS3Upload/useS3Upload.ts @@ -267,7 +267,7 @@ const useS3Upload = (caseId: string) => { return presignedPost } - const promises = files.map(async (file, idx) => { + const promises = files.map(async (file) => { try { updateFile({ ...file, status: 'uploading' }) @@ -279,13 +279,13 @@ const useS3Upload = (caseId: string) => { const newFileId = await addFileToCaseState({ ...file, - key: presignedPost.fields.key, + key: presignedPost.key, }) updateFile( { ...file, - key: presignedPost.fields.key, + key: presignedPost.key, percent: 100, status: 'done', }, diff --git a/apps/judicial-system/web/src/utils/hooks/useSections/index.ts b/apps/judicial-system/web/src/utils/hooks/useSections/index.ts index 7ee6ca835c9c..8d0c0f1363ff 100644 --- a/apps/judicial-system/web/src/utils/hooks/useSections/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useSections/index.ts @@ -15,6 +15,7 @@ import { isInvestigationCase, isProsecutionUser, isRestrictionCase, + isTrafficViolationCase, } from '@island.is/judicial-system/types' import { core, sections } from '@island.is/judicial-system-web/messages' import { RouteSection } from '@island.is/judicial-system-web/src/components/PageLayout/PageLayout' @@ -30,10 +31,7 @@ import { import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import { stepValidations, stepValidationsType } from '../../formHelper' -import { - isTrafficViolationIndictment, - shouldUseAppealWithdrawnRoutes, -} from '../../stepHelper' +import { shouldUseAppealWithdrawnRoutes } from '../../stepHelper' const validateFormStepper = ( isActiveSubSectionValid: boolean, @@ -401,7 +399,7 @@ const useSections = ( const { id, type, state } = workingCase const caseHasBeenReceivedByCourt = state === CaseState.RECEIVED || state === CaseState.MAIN_HEARING - const isTrafficViolation = isTrafficViolationIndictment(workingCase) + const isTrafficViolation = isTrafficViolationCase(workingCase) return { name: formatMessage(sections.indictmentCaseProsecutorSection.title), diff --git a/apps/judicial-system/web/src/utils/mocks.ts b/apps/judicial-system/web/src/utils/mocks.ts index 5a1e4d803f32..83b75c15eb91 100644 --- a/apps/judicial-system/web/src/utils/mocks.ts +++ b/apps/judicial-system/web/src/utils/mocks.ts @@ -167,7 +167,7 @@ export const mockCase = (caseType: CaseType): Case => { name: 'Donald Duck', gender: Gender.MALE, address: 'Batcave 1337', - defendantWaivesRightToCounsel: false, + defenderChoice: null, }, ], defendantWaivesRightToCounsel: false, diff --git a/apps/judicial-system/web/src/utils/stepHelper.ts b/apps/judicial-system/web/src/utils/stepHelper.ts index 50f0a8bb8abe..e410eac190bb 100644 --- a/apps/judicial-system/web/src/utils/stepHelper.ts +++ b/apps/judicial-system/web/src/utils/stepHelper.ts @@ -3,12 +3,9 @@ import parseISO from 'date-fns/parseISO' import { TagVariant } from '@island.is/island-ui/core' import { formatDate } from '@island.is/judicial-system/formatters' -import { isTrafficViolationCase } from '@island.is/judicial-system/types' import { CaseAppealState, CaseCustodyRestrictions, - CaseFileCategory, - CaseType, DefendantPlea, Gender, Notification, @@ -89,23 +86,6 @@ export const createCaseResentExplanation = ( }Krafa endursend ${formatDate(now, 'PPPp')} - ${explanation}` } -export const isTrafficViolationIndictment = (workingCase: Case): boolean => { - const isTrafficViolation = isTrafficViolationCase( - workingCase.indictmentSubtypes, - workingCase.type as CaseType, - ) - - return Boolean( - isTrafficViolation && - !( - workingCase.caseFiles && - workingCase.caseFiles.find( - (file) => file.category === CaseFileCategory.INDICTMENT, - ) - ), - ) -} - export const hasSentNotification = ( notificationType: NotificationType, notifications?: Notification[] | null, diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index 13e371364c45..8cba7111952a 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -1,6 +1,7 @@ // TODO: Add tests import { isIndictmentCase, + isTrafficViolationCase, prosecutorCanSelectDefenderForInvestigationCase, } from '@island.is/judicial-system/types' import { @@ -8,12 +9,13 @@ import { CaseAppealState, CaseFileCategory, CaseType, + DefenderChoice, SessionArrangements, User, } from '@island.is/judicial-system-web/src/graphql/schema' import { TempCase as Case } from '@island.is/judicial-system-web/src/types' -import { isBusiness, isTrafficViolationIndictment } from './stepHelper' +import { isBusiness } from './stepHelper' export type Validation = | 'empty' @@ -421,7 +423,7 @@ export const isDefenderStepValid = (workingCase: Case): boolean => { const defendantsAreValid = () => workingCase.defendants?.every((defendant) => { return ( - defendant.defendantWaivesRightToCounsel || + defendant.defenderChoice === DefenderChoice.WAIVE || validate([ [defendant.defenderName, ['empty']], [defendant.defenderEmail, ['email-format']], @@ -433,6 +435,11 @@ export const isDefenderStepValid = (workingCase: Case): boolean => { return Boolean(workingCase.prosecutor && defendantsAreValid()) } +export const isConclusionStepValid = (workingCase: Case): boolean => { + // TODO: Implement after selected action has been added as a field to the case + return true +} + export const isAdminUserFormValid = (user: User): boolean => { return Boolean( user.institution && @@ -495,7 +502,7 @@ export const isCaseFilesStepValidIndictments = (workingCase: Case): boolean => { workingCase.caseFiles?.some( (file) => file.category === CaseFileCategory.COVER_LETTER, ) && - (isTrafficViolationIndictment(workingCase) || + (isTrafficViolationCase(workingCase) || workingCase.caseFiles?.some( (file) => file.category === CaseFileCategory.INDICTMENT, )) && diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts index 096adb246d21..97b98e2d2f51 100644 --- a/apps/native/app/src/graphql/client.ts +++ b/apps/native/app/src/graphql/client.ts @@ -157,13 +157,7 @@ const cache = new InMemoryCache({ Query: { fields: { userNotifications: { - merge(existing, incoming) { - return { - ...existing, - ...incoming, - data: incoming.data || existing.data, - } - }, + merge: true, }, }, }, diff --git a/apps/native/app/src/screens/notifications/notifications.tsx b/apps/native/app/src/screens/notifications/notifications.tsx index 208655e1a3cf..f496cab9a3e5 100644 --- a/apps/native/app/src/screens/notifications/notifications.tsx +++ b/apps/native/app/src/screens/notifications/notifications.tsx @@ -5,7 +5,7 @@ import { Skeleton, Problem, } from '@ui' -import { Reference, useApolloClient } from '@apollo/client' +import { useApolloClient } from '@apollo/client' import { dismissAllNotificationsAsync } from 'expo-notifications' import React, { useCallback, useEffect, useMemo, useState } from 'react' @@ -87,40 +87,22 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ const [markAllUserNotificationsAsRead] = useMarkAllNotificationsAsReadMutation({ - onCompleted: (data) => { - if (data.markAllNotificationsRead?.success) { + onCompleted: (d) => { + if (d.markAllNotificationsRead?.success) { // If all notifications are marked as read, update cache to reflect that - client.cache.modify({ - fields: { - userNotifications(existingNotifications = {}) { - const existingDataRefs = existingNotifications.data || [] - - const updatedData = existingDataRefs.forEach( - (ref: Reference | NotificationItem) => { - const id = client.cache.identify(ref) - client.cache.modify({ - id, - fields: { - metadata(existingMetadata) { - return { - ...existingMetadata, - read: false, - } - }, - }, - }) - return ref - }, - ) - - return { - ...existingNotifications, - data: updatedData, - unreadCount: 0, - } + for (const notification of data?.userNotifications?.data || []) { + client.cache.modify({ + id: client.cache.identify(notification), + fields: { + metadata(existingMetadata) { + return { + ...existingMetadata, + read: true, + } + }, }, - }, - }) + }) + } } }, }) @@ -251,6 +233,7 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ title={intl.formatMessage({ id: 'notifications.screenTitle' })} onClosePress={() => Navigation.dismissModal(componentId)} style={{ marginHorizontal: 16 }} + showLoading={loading && !!data} /> @@ -281,7 +264,10 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ style={{ maxWidth: 145, }} - iconStyle={{ tintColor: theme.color.blue400 }} + iconStyle={{ + tintColor: theme.color.blue400, + resizeMode: 'contain', + }} /> {showError ? ( diff --git a/apps/native/app/src/screens/settings/settings.tsx b/apps/native/app/src/screens/settings/settings.tsx index 59473d9c2035..c63bcadacf9d 100644 --- a/apps/native/app/src/screens/settings/settings.tsx +++ b/apps/native/app/src/screens/settings/settings.tsx @@ -39,7 +39,6 @@ import { createNavigationOptionHooks } from '../../hooks/create-navigation-optio import { navigateTo } from '../../lib/deep-linking' import { showPicker } from '../../lib/show-picker' import { authStore } from '../../stores/auth-store' -import { useNotificationsStore } from '../../stores/notifications-store' import { preferencesStore, usePreferencesStore, diff --git a/apps/native/app/src/ui/lib/button/button.tsx b/apps/native/app/src/ui/lib/button/button.tsx index 98764be7a21e..db882a0c1a7e 100644 --- a/apps/native/app/src/ui/lib/button/button.tsx +++ b/apps/native/app/src/ui/lib/button/button.tsx @@ -92,7 +92,7 @@ const Text = styled.Text<{ const Icon = styled.Image` width: 16px; height: 16px; - margin-left: 10px; + margin-left: 8px; ` export function Button({ diff --git a/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.css.ts b/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.css.ts new file mode 100644 index 000000000000..3ae765b21300 --- /dev/null +++ b/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.css.ts @@ -0,0 +1,13 @@ +import { theme } from '@island.is/island-ui/theme' +import { style } from '@vanilla-extract/css' + +export const lock = style({ + position: 'absolute', + zIndex: 1, + top: theme.spacing[2], + right: theme.spacing[3], +}) + +export const img = style({ + height: 180, +}) diff --git a/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.tsx b/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.tsx new file mode 100644 index 000000000000..516d77d1db4b --- /dev/null +++ b/apps/service-portal/src/components/DocumentsEmpty/DocumentsEmpty.tsx @@ -0,0 +1,59 @@ +import { Box, Icon, Text } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { m } from '@island.is/service-portal/core' +import * as styles from './DocumentsEmpty.css' + +interface Props { + hasDelegationAccess: boolean +} + +export const DocumentsEmpty = ({ hasDelegationAccess }: Props) => { + const { formatMessage } = useLocale() + + return ( + + + {hasDelegationAccess + ? formatMessage(m.emptyDocumentsList) + : formatMessage(m.accessNeeded)} + + {!hasDelegationAccess && ( + + {formatMessage(m.accessDeniedText)} + + )} + + { + No access + } + + {!hasDelegationAccess && ( + + )} + + ) +} + +export default DocumentsEmpty diff --git a/apps/service-portal/src/components/Header/Header.tsx b/apps/service-portal/src/components/Header/Header.tsx index 5bd6d74d1eab..21c913b99da0 100644 --- a/apps/service-portal/src/components/Header/Header.tsx +++ b/apps/service-portal/src/components/Header/Header.tsx @@ -26,6 +26,7 @@ import { useWindowSize } from 'react-use' import NotificationButton from '../Notifications/NotificationButton' import Sidemenu from '../Sidemenu/Sidemenu' import * as styles from './Header.css' +import { DocumentsScope } from '@island.is/auth/scopes' export type MenuTypes = 'side' | 'user' | 'notifications' | undefined interface Props { @@ -57,6 +58,10 @@ export const Header = ({ position }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const hasNotificationsDelegationAccess = user?.scopes?.includes( + DocumentsScope.main, + ) + return (
@@ -96,7 +101,6 @@ export const Header = ({ position }: Props) => { flexWrap="nowrap" marginLeft={[1, 1, 2]} > - {user && } { setMenuOpen(val)} showMenu={menuOpen === 'notifications'} + disabled={!hasNotificationsDelegationAccess} /> )} + {user && } + + + ) : undefined} + {petitionPageInfo?.hasNextPage ? ( + + + + ) : undefined} + ) } diff --git a/libs/shared/connected/src/lib/generalPetition/GeneralPetitionLists/useGetPetitionLists.ts b/libs/shared/connected/src/lib/generalPetition/GeneralPetitionLists/useGetPetitionLists.ts index 0ac4afba5128..6e476f71c4ae 100644 --- a/libs/shared/connected/src/lib/generalPetition/GeneralPetitionLists/useGetPetitionLists.ts +++ b/libs/shared/connected/src/lib/generalPetition/GeneralPetitionLists/useGetPetitionLists.ts @@ -1,5 +1,6 @@ import gql from 'graphql-tag' import { useQuery } from '@apollo/client' +import { useState } from 'react' interface PetitionListResponse { endorsementSystemGetGeneralPetitionLists: any @@ -54,21 +55,73 @@ const GetGeneralPetitionListEndorsements = gql` ` export const useGetPetitionLists = () => { - const { data: endorsementListsResponse } = useQuery( + const [pagination, setPagination] = useState({ after: '', before: '' }) + const { data, loading, error, fetchMore } = useQuery( GetGeneralPetitionLists, { variables: { input: { tags: 'generalPetition', - limit: 1000, + after: pagination.after, + before: pagination.before, + limit: 10, }, }, }, ) - return ( - endorsementListsResponse?.endorsementSystemGetGeneralPetitionLists ?? [] - ) + const loadNextPage = () => { + if (data?.endorsementSystemGetGeneralPetitionLists.pageInfo.hasNextPage) { + setPagination({ + ...pagination, + after: data.endorsementSystemGetGeneralPetitionLists.pageInfo.endCursor, + before: '', + }) + fetchMore({ + variables: { + input: { + tags: 'generalPetition', + limit: 10, + after: + data.endorsementSystemGetGeneralPetitionLists.pageInfo.endCursor, + }, + }, + }) + } + } + + const loadPreviousPage = () => { + if ( + data?.endorsementSystemGetGeneralPetitionLists.pageInfo.hasPreviousPage + ) { + setPagination({ + ...pagination, + after: '', + before: + data.endorsementSystemGetGeneralPetitionLists.pageInfo.startCursor, + }) + fetchMore({ + variables: { + input: { + tags: 'generalPetition', + limit: 10, + before: + data.endorsementSystemGetGeneralPetitionLists.pageInfo + .startCursor, + }, + }, + }) + } + } + + return { + data: data?.endorsementSystemGetGeneralPetitionLists?.data ?? [], + loading, + error, + loadNextPage, + loadPreviousPage, + pageInfo: data?.endorsementSystemGetGeneralPetitionLists.pageInfo, + } } export const useGetPetitionListEndorsements = (listId: string) => { diff --git a/libs/shared/constants/src/lib/organizationSlug.ts b/libs/shared/constants/src/lib/organizationSlug.ts index c14e159d6815..a64995f2d83d 100644 --- a/libs/shared/constants/src/lib/organizationSlug.ts +++ b/libs/shared/constants/src/lib/organizationSlug.ts @@ -45,3 +45,4 @@ export type OrganizationSlugType = | 'vegagerdin' | 'landlaeknir' | 'hugverkastofan' + | 'geislavarnir-rikisins' diff --git a/libs/shared/types/src/lib/api-cms-domain.ts b/libs/shared/types/src/lib/api-cms-domain.ts index f36ccc37be0d..c5b724a2a73a 100644 --- a/libs/shared/types/src/lib/api-cms-domain.ts +++ b/libs/shared/types/src/lib/api-cms-domain.ts @@ -7,3 +7,12 @@ export enum CustomPageUniqueIdentifier { OfficialJournalOfIceland = 'OfficialJournalOfIceland', Vacancies = 'Vacancies', } + +export interface StatisticSourceValue { + header: string + value: number | null +} + +export type StatisticSourceData = { + data: Record +} diff --git a/libs/shared/utils/src/lib/simpleEncryption.spec.ts b/libs/shared/utils/src/lib/simpleEncryption.spec.ts index 3cb53550d976..1d25b93164dd 100644 --- a/libs/shared/utils/src/lib/simpleEncryption.spec.ts +++ b/libs/shared/utils/src/lib/simpleEncryption.spec.ts @@ -1,29 +1,34 @@ +/** + * @jest-environment node + */ + import { maskString, unmaskString } from './simpleEncryption' const originalText = 'Original Jest Text!' const secretKey = 'not-really-secret-key' describe('Encryption and Decryption Functions', () => { - test('Encrypt and decrypt a string successfully', () => { - const encrypted = maskString(originalText, secretKey) + test('Encrypt and decrypt a string successfully', async () => { + const encrypted = await maskString(originalText, secretKey) // Check for successful encryption expect(encrypted).not.toBe(originalText) + expect(encrypted).not.toBeNull() + + // If null check succeeds, we can safely cast to string for the unmasking test + const textToDecrypt = encrypted as string - // Check for successful decryption - if (encrypted !== null) { - const decrypted = unmaskString(encrypted, secretKey) - expect(decrypted).toBe(originalText) - expect(encrypted).not.toBe(originalText) - } else { - // Fail the test explicitly if encryption failed - fail('Encryption failed') - } + const decrypted = await unmaskString(textToDecrypt, secretKey) + expect(decrypted).toBe(originalText) + expect(encrypted).not.toBe(originalText) }) - test('Return null in case of decryption failure', () => { + test('Return null in case of decryption failure', async () => { // Example: testing decryption failure - const decryptedFailure = unmaskString('invalid-encrypted-text', secretKey) + const decryptedFailure = await unmaskString( + 'invalid-encrypted-text', + secretKey, + ) expect(decryptedFailure).toBeNull() }) }) diff --git a/libs/shared/utils/src/lib/simpleEncryption.ts b/libs/shared/utils/src/lib/simpleEncryption.ts index 472c7c00d9b6..9d2d4cf0f010 100644 --- a/libs/shared/utils/src/lib/simpleEncryption.ts +++ b/libs/shared/utils/src/lib/simpleEncryption.ts @@ -1,59 +1,108 @@ -import { Base64 } from 'js-base64' -import { createCipheriv, createDecipheriv, createHash } from 'crypto' +const ALGORITHM = 'AES-CBC' +const DELIMITER = ':' // Delimiter to separate IV and encrypted text -const ALGORITHM = 'aes-256-cbc' +const crypto = global.window ? window.crypto : require('crypto') + +// Function to convert ArrayBuffer to base64 +const bufferToBase64 = (buffer: ArrayBuffer): string => { + return btoa(String.fromCharCode(...new Uint8Array(buffer))) +} + +// Function to convert base64 to ArrayBuffer +const base64ToBuffer = (base64: string): ArrayBuffer => { + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer +} + +const str2ab = (str: string): ArrayBuffer => { + const encoder = new TextEncoder() + return encoder.encode(str) +} + +// Function to derive a cryptographic key from a text password +const deriveKey = async (password: string): Promise => { + const keyMaterial = await crypto.subtle.importKey( + 'raw', + str2ab(password), + { name: 'PBKDF2' }, + false, + ['deriveKey'], + ) + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + // we do not need to hide this in environment variables since we are not using it for secure encryption but rather to mask strings, so they don't show up in logs + salt: new TextEncoder().encode('unique-salt-value=='), + iterations: 1000, + hash: 'SHA-256', + }, + keyMaterial, + { name: ALGORITHM, length: 256 }, + true, + ['encrypt', 'decrypt'], + ) +} /** * * @param text The string you wish to hide * @param key You secret key - * @returns URL safe Base64 */ -export const maskString = (text: string, key: string): string | null => { +export const maskString = async ( + text: string, + key: string, +): Promise => { try { - const derivedKey = hashKey(key) + const derivedKey = await deriveKey(key) + const iv = crypto.getRandomValues(new Uint8Array(16)) // AES-CBC recommended IV length is 16 bytes - const cipher = createCipheriv(ALGORITHM, derivedKey, Buffer.alloc(16)) - const encrypted = - cipher.update(text, 'utf-8', 'base64') + cipher.final('base64') + const encrypted = await crypto.subtle.encrypt( + { + name: ALGORITHM, + iv: iv, + }, + derivedKey, + str2ab(text), + ) + const ivStr = bufferToBase64(iv) + const encryptedStr = bufferToBase64(encrypted) - return Base64.encodeURI(encrypted) + return encodeURIComponent(ivStr + DELIMITER + encryptedStr) } catch (e) { - console.error({ - name: 'unmaskString', - error: e, - }) + console.error(e) return null } } /** - * @param encryptedText Base64 returned from encrypt() - * @param key The secret key you used in encrypt() - * @returns Reveals the string hidden by encrypt() + * @param encryptedText Base64 returned from maskString() + * @param key The secret key you used in maskString() + * @returns Reveals the string hidden by maskString() */ -export const unmaskString = ( +export const unmaskString = async ( encryptedText: string, key: string, -): string | null => { +): Promise => { try { - const encryptedData = Base64.decode(encryptedText) - const derivedKey = hashKey(key) - const decipher = createDecipheriv(ALGORITHM, derivedKey, Buffer.alloc(16)) + encryptedText = decodeURIComponent(encryptedText) + const [ivPart, encryptedPart] = encryptedText.split(DELIMITER) + const iv = base64ToBuffer(ivPart) + + const encrypted = base64ToBuffer(encryptedPart) - return ( - decipher.update(encryptedData, 'base64', 'utf-8') + - decipher.final('utf-8') + const derivedKey = await deriveKey(key) + const decrypted = await crypto.subtle.decrypt( + { + name: ALGORITHM, + iv: iv, + }, + derivedKey, + encrypted, ) + + const decoder = new TextDecoder() + return decoder.decode(decrypted) } catch (e) { - console.error({ - name: 'unmaskString', - error: e, - }) + console.error(e) return null } } - -function hashKey(key: string): Buffer { - return createHash('sha256').update(key).digest() -} diff --git a/libs/university-gateway/src/lib/model/application.ts b/libs/university-gateway/src/lib/model/application.ts index 7855d9f0fa8e..3b927eab4870 100644 --- a/libs/university-gateway/src/lib/model/application.ts +++ b/libs/university-gateway/src/lib/model/application.ts @@ -13,6 +13,7 @@ export interface IApplication { workExperienceList: IApplicationWorkExperience[] extraFieldList: IApplicationExtraFields[] educationOption?: string + attachments?: Array } export interface IApplicationApplicant { @@ -47,6 +48,12 @@ export interface IApplicationWorkExperience { jobTitle: string } +export interface IApplicationAttachment { + fileName: string + fileType: string + blob: Blob +} + export interface IApplicationExtraFields { key: string value: object diff --git a/scripts/ci/00_prepare-base-tags.sh b/scripts/ci/00_prepare-base-tags.sh index c26708a24fc3..4231289a1658 100755 --- a/scripts/ci/00_prepare-base-tags.sh +++ b/scripts/ci/00_prepare-base-tags.sh @@ -10,6 +10,7 @@ cp -r "$ROOT/.github/actions/dist/." "$tempRepo" LAST_GOOD_BUILD=$(DEBUG="*,-simple-git" REPO_ROOT="$ROOT" node $tempRepo/main.js) if echo "$LAST_GOOD_BUILD" | grep -q 'full_rebuild_needed'; then export NX_AFFECTED_ALL=true + echo "NX_AFFECTED_ALL=$NX_AFFECTED_ALL" >> $GITHUB_ENV exit 0 fi echo "Stickman done" diff --git a/scripts/ci/10_prepare-docker-deps.sh b/scripts/ci/10_prepare-docker-deps.sh index a0ff050121a3..d0513560c150 100755 --- a/scripts/ci/10_prepare-docker-deps.sh +++ b/scripts/ci/10_prepare-docker-deps.sh @@ -7,11 +7,15 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" source "$DIR"/_common.sh mkdir -p "$PROJECT_ROOT"/cache + +NODE_IMAGE_TAG=${NODE_IMAGE_TAG:-$(./scripts/ci/get-node-version.mjs)} + docker buildx create --driver docker-container --use || true docker buildx build \ --platform=linux/amd64 \ --cache-to=type=local,dest="$PROJECT_ROOT"/cache \ + --build-arg NODE_IMAGE_TAG="$NODE_IMAGE_TAG" \ -f "${DIR}"/Dockerfile \ --target=deps \ "$PROJECT_ROOT" @@ -20,6 +24,7 @@ docker buildx build \ --platform=linux/amd64 \ --cache-from=type=local,src="$PROJECT_ROOT"/cache \ --cache-to=type=local,dest="$PROJECT_ROOT"/cache_output \ + --build-arg NODE_IMAGE_TAG="$NODE_IMAGE_TAG" \ -f "${DIR}"/Dockerfile \ --target=output-base \ "$PROJECT_ROOT" diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index e5c182e323eb..0d482c1203d7 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -1,7 +1,8 @@ # This is a multi-stage Dockerfile which contains all CI-related operations as well as images to be deployed in production ARG PLAYWRIGHT_VERSION ARG DOCKER_IMAGE_REGISTRY=public.ecr.aws -FROM $DOCKER_IMAGE_REGISTRY/docker/library/node:18-alpine3.15 as deps +ARG NODE_IMAGE_TAG +FROM ${DOCKER_IMAGE_REGISTRY}/docker/library/node:${NODE_IMAGE_TAG} as deps RUN apk add -U git @@ -32,7 +33,7 @@ ENV NODE_OPTIONS="--max-old-space-size=8192" RUN yarn run build ${APP} --prod -FROM $DOCKER_IMAGE_REGISTRY/docker/library/node:18-alpine3.15 as output-base +FROM ${DOCKER_IMAGE_REGISTRY}/docker/library/node:${NODE_IMAGE_TAG} as output-base # this is base image for containers that are to be deployed ARG GIT_BRANCH ARG GIT_SHA diff --git a/scripts/ci/_common.mjs b/scripts/ci/_common.mjs new file mode 100644 index 000000000000..b52c5bd44126 --- /dev/null +++ b/scripts/ci/_common.mjs @@ -0,0 +1,11 @@ +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +export const ROOT = resolve(__dirname, '..', '..') + +export async function getPackageJSON(filePath = resolve(ROOT, 'package.json')) { + const content = JSON.parse(await readFile(filePath, 'utf-8')) + return content +} diff --git a/scripts/ci/_nx-affected-targets.sh b/scripts/ci/_nx-affected-targets.sh index 5ba344dde326..3d861a2a0194 100755 --- a/scripts/ci/_nx-affected-targets.sh +++ b/scripts/ci/_nx-affected-targets.sh @@ -8,12 +8,13 @@ source "$DIR"/_common.sh export HEAD=${HEAD:-HEAD} export BASE=${BASE:-main} NX_AFFECTED_ALL=${NX_AFFECTED_ALL:-} +TEST_EVERYTHING=${TEST_EVERYTHING:-} # This is a helper script to find NX affected projects for a specific target AFFECTED_ALL=${AFFECTED_ALL:-} # Could be used for forcing all projects to be affected (set or create `secret` in GitHub with the name of this variable set to the name of the branch that should be affected, prefixed with the magic string `7913-`) BRANCH=${BRANCH:-$GITHUB_HEAD_REF} -if [[ (-n "$BRANCH" && -n "$AFFECTED_ALL" && "$AFFECTED_ALL" == "7913-$BRANCH") || (-n "$NX_AFFECTED_ALL" && "$NX_AFFECTED_ALL" == "true") ]]; then +if [[ (-n "$BRANCH" && -n "$AFFECTED_ALL" && "$AFFECTED_ALL" == "7913-$BRANCH") || (-n "$NX_AFFECTED_ALL" && "$NX_AFFECTED_ALL" == "true") || (-n "$TEST_EVERYTHING" && "$TEST_EVERYTHING" == "true")]]; then EXTRA_ARGS="" else EXTRA_ARGS=(--affected --base "$BASE" --head "$HEAD") diff --git a/scripts/ci/get-node-modules-hash.mjs b/scripts/ci/get-node-modules-hash.mjs new file mode 100755 index 000000000000..45ee96b9bb3e --- /dev/null +++ b/scripts/ci/get-node-modules-hash.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { arch, platform } from 'os' +import crypto from 'node:crypto' + +import { ROOT, getPackageJSON } from './_common.mjs' + +process.stdout.write( + `${getPlatformString()}-${await getYarnLockHash()}-${await getPackageHash()}-${await getNodeVersionString()}`, +) + +async function getNodeVersionString() { + const content = await getPackageJSON() + const nodeVersion = content?.engines?.node + const yarnVersion = content?.engines?.yarn + if (!nodeVersion) { + throw new Error('Node version not defined') + } + if (!yarnVersion) { + throw new Error('Yarn version not defined') + } + + return `${nodeVersion}-${yarnVersion}` +} + +function getPlatformString() { + return `${platform()}-${arch()}` +} + +async function getPackageHash( + keys = ['resolutions', 'dependencies', 'devDependencies'], +) { + const content = await getPackageJSON() + const value = keys.reduce((a, b) => { + return { + ...a, + [b]: content[b], + } + }, {}) + return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex') +} + +async function getYarnLockHash(filePath = resolve(ROOT, 'yarn.lock')) { + const content = await readFile(filePath, 'utf-8') + return crypto.createHash('sha256').update(content).digest('hex') +} diff --git a/scripts/ci/get-node-version.mjs b/scripts/ci/get-node-version.mjs new file mode 100755 index 000000000000..0c7de8b3e6c0 --- /dev/null +++ b/scripts/ci/get-node-version.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +import { getPackageJSON } from './_common.mjs' + +const DOCKERHUB_BASE_URL = + 'https://hub.docker.com/v2/repositories/library/node/tags?page_size=100' + +const nodeVersion = await getPackageVersion() +const version = await getVersion(nodeVersion) + +if (!version) { + console.error(`Failed getting docker image for ${nodeVersion}`) + process.exit(1) +} +process.stdout.write(version) + +async function getVersion( + version, + withAlpine = true, + architecture = 'amd64', + url = null, +) { + try { + const baseURL = url ?? DOCKERHUB_BASE_URL + const response = await fetch(baseURL) + const data = await response.json() + + const filteredTags = data.results.filter((tag) => { + const isVersionMatch = tag.name.startsWith(version) + const isAlpine = withAlpine ? tag.name.includes('alpine') : true + const isArchitectureMatch = tag.images.some( + (image) => image.architecture === architecture, + ) + return isVersionMatch && isAlpine && isArchitectureMatch + }) + + const latestTag = filteredTags.sort((a, b) => + b.last_updated.localeCompare(a.last_updated), + )[0] + + if (latestTag) { + return latestTag.name + } + const nextUrl = data.next + if (!nextUrl) { + return null + } + return getVersion(version, withAlpine, architecture, nextUrl) + } catch (error) { + console.error('Failed to fetch the Docker tags', error) + } +} + +async function getPackageVersion() { + const content = await getPackageJSON() + const version = content.engines?.node + if (!version) { + throw new Error(`Cannot find node version`) + } + return version +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f85e58b9c43..644570efaf56 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -748,6 +748,9 @@ "@island.is/clients/transport-authority/vehicle-printing": [ "libs/clients/transport-authority/vehicle-printing/src/index.ts" ], + "@island.is/clients/ultraviolet-radiation": [ + "libs/clients/ultraviolet-radiation/src/index.ts" + ], "@island.is/clients/university-application/agricultural-university-of-iceland": [ "libs/clients/university-application/agricultural-university-of-iceland/src/index.ts" ], @@ -872,6 +875,9 @@ "@island.is/judicial-system/formatters": [ "libs/judicial-system/formatters/src/index.ts" ], + "@island.is/judicial-system/lawyers": [ + "libs/judicial-system/lawyers/src/index.ts" + ], "@island.is/judicial-system/message": [ "libs/judicial-system/message/src/index.ts" ],